How can I ensure that the end of my custom quiljs parchment doesn't continue the formatting? - quill

I have a custom parchment that looks like:
import { Quill } from 'react-quill';
const Parchment = Quill.import('parchment');
let config = { scope: Parchment.Scope.INLINE };
let AcceptedPredictionClass = new Parchment.Attributor.Class('accepted', 'ql', config);
Quill.register(AcceptedPredictionClass)
and to use it:
const delta = new Delta()
.retain(currentSelection.index)
.delete(predictionLength)
.insert(previousPredictionText, { accepted: 'accepted' })
quill.updateContents(delta)
but the problem is that if I start typing, it keeps the ql-accepted style. I need it to revert back to normal.

How about simply adding one more .insert(' ', {}) after last insert? This should add one normal span after the inserted class.
This is how it will be:
const delta = new Delta()
.retain(currentSelection.index)
.delete(predictionLength)
.insert(previousPredictionText, { accepted: 'accepted' })
.insert(' ', {})
quill.updateContents(delta)
FYI: I haven't tested it yet but the general idea is that the cursor will be inside new span without the added class.

Related

Can I remove parameter from URL generated by video playlist in Elementor

Elementor Pro has a new widget, video playlist. It appends a parameter to the URL, like so: http://aaronpenton.net/ampcreative/vip/about-vip/?playlist=f68425e&video=b8a9967
This is obviously terrible for SEO and UX. Is there a way to remove the ?playlist=f68425e&video=b8a9967 ?
My brother help me with the next script.
Put a "HTML Elementor Widget" with the following:
<script>
function getURLParameter(name) {
return decodeURI((RegExp(name + '=' + '(.+?)(&|$)').exec(location.search)||[,null])[1]);
}
function hideURLParams() {
//Parameters to hide (ie ?playlist=value, ?video=value, etc)
var hide = ['playlist','video'];
for(var h in hide) {
if(getURLParameter(h)) {
history.replaceState(null, document.getElementsByTagName("title")[0].innerHTML, window.location.pathname);
}
}
}
window.onload = hideURLParams;
</script>
This should do the trick.
const url = 'http://aaronpenton.net/ampcreative/vip/about-vip/?playlist=f68425e&video=b8a9967'
url.replace(url.split('/')[6], '')
split, well.. splits the string into an array by the /
character.
At index 6 the array contains the ?playlist=f68425e&video=b8a9967 substring which can than be removed (i.e. replaced by the empty string) using replace.
A more general approach to removing the last part of the url might
be to use the array length instead of specifying the index:
const url = 'http://aaronpenton.net/ampcreative/vip/about-vip/?playlist=f68425e&video=b8a9967'
const urlArr = url.split('/')
url.replace(urlArr[urlArr.length - 1], '')
Update:
Another way to do this is using the URL API
const url = new URL('http://aaronpenton.net/ampcreative/vip/about-vip/?playlist=f68425e&video=b8a9967');
const result = url.origin + url.pathname
or in a function:
const removeParameter = u => {
const url = new URL(u);
return url.origin + url.pathname
}
You might have to check the specification for further details (browser support etc)

Adding Entity into its own block in DraftJS

So I'm trying to create a fairly simple WYSIWYG BBCode editor for my project as an opportunity to wrap my mind around DraftJS. I've been following some tutorials and also using react-rte as an example (since it has 99% of the functionality I need and looks relatively simple to understand).
The problem I've stumped on is that react-rte inserts image entities inline (it adds a space in the current selection and then ties an entry to that space) like this:
const addEntity = (editorState, type, mutability = 'MUTABLE', data = {}) => {
let currentContent = editorState.getCurrentContent();
let selection = editorState.getSelection();
currentContent = currentContent.createEntity(type, mutability, data);
let entityKey = currentContent.getLastCreatedEntityKey();
return Modifier.insertText(currentContent, selection, ' ', null, entityKey);
}
I want each image (and video alike) to be in its own separate block and make it so nothing else can be written to that block.
I have found an example of the behavior I'm after in megadraft, but I have been unable to work my way through its code to find the correct implementation.
Found the solution after many hours of trial and error (and lots of manuals).
const addAtomic = (editorState, type, mutability = 'MUTABLE', data = {}) => {
let currentContent = editorState.getCurrentContent();
let selection = editorState.getSelection();
currentContent = currentContent.createEntity(type, mutability, data);
let entityKey = currentContent.getLastCreatedEntityKey();
const newState = EditorState.set(editorState, { currentContent: currentContent })
return AtomicBlockUtils.insertAtomicBlock(newState, entityKey, ' ')
}

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

Convert Quill Delta to HTML

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>'

Binding Angular2 components inside of a Jquery plugin template

I'm working on using a kendo inside of an angular 2 project.
Getting the widget set up correctly is no problem:
ngOnInit() {
let options = inputsToOptionObject(KendoUIScheduler, this);
options.dataBound = this.bound;
this.scheduler = $(this.element.nativeElement)
.kendoScheduler(options)
.data('kendoScheduler');
}
When that runs, the plugin modifies the DOM (and, to my knowleged, without modifiying the shadow DOM maintained by angular2). My issue is that if I want to use a component anywhere inside of the plugin, like in a template, Angular is unaware of it's existence and won't bind it.
Example:
public views:kendo.ui.SchedulerView[] = [{
type: 'month',
title: 'test',
dayTemplate: (x:any) => {
let date = x.date.getDate();
let count = this.data[date];
return `<monthly-scheduler-day [date]="test" [count]=${count}"></monthly-scheduler-day>`
}
}];
The monthly-scheduler-day class:
#Component({
selector: 'monthly-scheduler-day',
template: `
<div>{{date}}</div>
<div class="badge" (click)=dayClick($event)>Available</div>
`
})
export class MonthlySchedulerDayComponent implements OnInit{
#Input() date: number;
#Input() count: number;
constructor() {
console.log('constructed');
}
ngOnInit(){
console.log('created');
}
dayClick(event){
console.log('clicked a day');
}
}
Is there a "right" way to bind these components inside of the markup created by the widget? I've managed to do it by listening for the bind event from the widget and then looping over the elements it created and using the DynamicComponentLoader, but it feels wrong.
I found some of the details I needed in this thread: https://github.com/angular/angular/issues/6223
I whipped this service up to handle binding my components:
import { Injectable, ComponentMetadata, ViewContainerRef, ComponentResolver, ComponentRef, Injector } from '#angular/core';
declare var $:JQueryStatic;
#Injectable()
export class JQueryBinder {
constructor(
private resolver: ComponentResolver,
private injector: Injector
){}
public bindAll(
componentType: any,
contextParser:(html:string)=>{},
componentInitializer:(c: ComponentRef<any>, context: {})=>void):
void
{
let selector = Reflect.getMetadata('annotations', componentType).find((a:any) => {
return a instanceof ComponentMetadata
}).selector;
this.resolver.resolveComponent(componentType).then((factory)=> {
$(selector).each((i,e) => {
let context = contextParser($(e).html());
let c = factory.create(this.injector, null, e);
componentInitializer(c, context);
c.changeDetectorRef.detectChanges();
c.onDestroy(()=>{
c.changeDetectorRef.detach();
})
});
});
}
}
Params:
componentType: The component class you want to bind. It uses reflection to pull the selector it needs
contextParser: callback that takes the existing child html and constructs a context object (anything you need to initialize the component state)
componentInitializer - callback that initializes the created component with the context you parsed
Example usage:
let parser = (html: string) => {
return {
date: parseInt(html)
};
};
let initer = (c: ComponentRef<GridCellComponent>, context: { date: number })=>{
let d = context.date;
c.instance.count = this.data[d];
c.instance.date = d;
}
this.binder.bindAll(GridCellComponent, parser, initer );
Well your solution works fine until the component needs to change its state and rerender some stuff.
Because I haven't found yet any ability to get ViewContainerRef for an element generated outside of Angular (jquery, vanilla js or even server-side)
the first idea was to call detectChanges() by setting up an interval. And after several iterations finally I came to a solution which works for me.
So far in 2017 you have to replace ComponentResolver with ComponentResolverFactory and do almost the same things:
let componentFactory = this.factoryResolver.resolveComponentFactory(componentType),
componentRef = componentFactory.create(this.injector, null, selectorOrNode);
componentRef.changeDetectorRef.detectChanges();
After that you can emulate attaching component instance to the change detection cycle by subscribing to EventEmitters of its NgZone:
let enumerateProperties = obj => Object.keys(obj).map(key => obj[key]),
properties = enumerateProperties(injector.get(NgZone))
.filter(p => p instanceof EventEmitter);
let subscriptions = Observable.merge(...properties)
.subscribe(_ => changeDetectorRef.detectChanges());
Of course don't forget to unsubscribe on destroy:
componentRef.onDestroy(_ => {
subscriptions.forEach(x => x.unsubscribe());
componentRef.changeDetectorRef.detach();
});
UPD after stackoverflowing once more
Forget all the words above. It works but just follow this answer

Resources