CKEditor 5 Convert Paragraph Model to Div with Inner Span - ckeditor5

I'm currently reading the CKEditor 5 docs, and trying to understand how I could achieve the following conversion:
For every paragraph model, convert the view to a div with an inner span.
The following gets me half way there...
editor.conversion.elementToElement({ model: 'paragraph', view: 'div', converterPriority: 'high' })
And now all paragraph models are converted to divs (instead of <p> elements)
But how can I add the additional span element so that each paragraph model is rendered as:
<div><span>Text here...</span></div>
Will I have to switch to more specialized upcast, dataDowncast, and editorDowncast converters? Or can this still be handled via editor.conversion.elementToElement?
Update:
I've tried the following - which is very close:
editor.conversion.for('downcast').elementToElement({
model: 'paragraph',
view: (modelElement, conversionApi) => {
const { writer } = conversionApi
const divElement = writer.createContainerElement('div')
const spanElement = writer.createAttributeElement('span')
conversionApi.mapper.bindElements(modelElement, spanElement)
writer.insert(writer.createPositionAt(divElement, 0), spanElement)
return divElement
},
converterPriority: 'high',
})
However, this outputs the following:
<div>
Text here...
<span></span>
</div>
Struggling to get the modelElement inside the span :-(

For anyone else looking for this, I've posted a working solution here... CKEditor 5 Downcast Converter for Paragraph To Wrap Text in Span

Related

How to add "target" attribute to `a` tag in ckeditor5?

I have create my own plugin for link. Now I want to add some other attributes to a tag generated by the plugin, like target, rel.
But I am not able to get it done. Here is the my plugins code for converter.
What converters I should add so that a tag can support other attributes?
/**
* #license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* #module link/linkediting
*/
import LinkEditing from '#ckeditor/ckeditor5-link/src/linkediting';
import {
downcastAttributeToElement
} from '#ckeditor/ckeditor5-engine/src/conversion/downcast-converters';
import { upcastElementToAttribute } from '#ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
import LinkCommand from './uclinkcommand';
import UnlinkCommand from './ucunlinkcommand';
import { createLinkElement } from '#ckeditor/ckeditor5-link/src/utils';
import { ensureSafeUrl } from './utils';
import bindTwoStepCaretToAttribute from '#ckeditor/ckeditor5-engine/src/utils/bindtwostepcarettoattribute';
/**
* The link engine feature.
*
* It introduces the `linkHref="url"` attribute in the model which renders to the view as a `<a href="url">` element.
*
* #extends module:core/plugin~Plugin
*/
export default class UcLinkEditing extends LinkEditing {
/**
* #inheritDoc
*/
init() {
const editor = this.editor;
// Allow link attribute on all inline nodes.
editor.model.schema.extend( '$text', { allowAttributes: 'linkHref' } );
editor.conversion.for( 'dataDowncast' )
.add( downcastAttributeToElement( { model: 'linkHref', view: createLinkElement } ) );
editor.conversion.for( 'editingDowncast' )
.add( downcastAttributeToElement( { model: 'linkHref', view: ( href, writer ) => {
return createLinkElement( ensureSafeUrl( href ), writer );
} } ) );
editor.conversion.for( 'upcast' )
.add( upcastElementToAttribute( {
view: {
name: 'a',
attribute: {
href: true
}
},
model: {
key: 'linkHref',
value: viewElement => viewElement.getAttribute( 'href' )
}
} ) );
// Create linking commands.
editor.commands.add( 'ucLink', new LinkCommand( editor ) );
editor.commands.add( 'ucUnlink', new UnlinkCommand( editor ) );
// Enable two-step caret movement for `linkHref` attribute.
bindTwoStepCaretToAttribute( editor.editing.view, editor.model, this, 'linkHref' );
// Setup highlight over selected link.
this._setupLinkHighlight();
}
}
Introduction
Before I get to the code I'd like to take the occasion to explain the CKEditor 5 approach to inline elements (like <a>) so that the solution is easier to understand. With that knowledge, similar problems in the future should not be troubling. Following is meant to be a comprehensive tutorial, so expect a long read.
Even though you may know most of the things in the theory part, I recommend reading it to get the full understanding of how things work in CKEditor 5.
Also, do note that I will present a solution for the original CKEditor 5 plugin as it will be more valuable to other community members seeking a tutorial on this matter. Still, I hope that with the insight from this tutorial you will be able to adjust the code sample to your custom plugin.
Also, keep in mind that this tutorial does not discuss UI part of this plugin, only how things should be configured for conversion purposes. Adding and removing attributes is the job for UI or for some other part of code. Here I discuss only engine stuff.
Inline elements in CKEditor 5
First, let's establish which elements are inline. By inline elements I understand elements like <strong>, <a> or <span>. Unlike <p>, <blockquote> or <div>, inline elements do not structure the data. Instead, they mark some text in a specific (visual and semantical) way. So, in a way, these elements are a characteristic of a given part of a text. As a result, we say that given part of a text is bold, or that given part of a text is/has a link.
Similarly, in the model, we don't represent <a> or <strong> directly as elements. Instead, we allow adding attributes to a part of a text. This is how text characteristics (as bold, italic or link) are represented.
For example, in the model, we might have a <paragraph> element with Foo bar text, where bar has the bold attribute set to true. We would note it this way: <paragraph>Foo <$text bold="true">bar</$text></paragraph>. See, that there is no <strong> or any other additional element there. It's just some text with an attribute. Later, the bold attribute is converted to <strong> element.
By the way: view elements that come from model attributes have their own class: view.AttributeElement and instead of inline elements can be also called attribute elements. Sadly, the name conflicts with "attribute" as an attribute of a view element (what is worse, attribute element is allowed to have attributes).
Of course, text may have multiple attributes and all of them are converted to their respective view inline elements. Keep in mind that in the model, attributes do not have any set order. This is contrary to the view or HTML, where inline elements are nested one in another. The nesting happens during conversion from the model to the view. This makes working in model simpler, as features do not need to take care of breaking or rearranging elements in the model.
Consider this model string:
<paragraph>
<$text bold="true">Foo </$text>
<$text bold="true" linkHref="bar.html">bar</$text>
<$text bold="true"> baz</$text>
</paragraph>
It is a bold Foo bar baz text with a link on bar. During conversion, it will be converted to:
<p>
<strong>Foo </strong><strong>bar</strong><strong> baz</strong>
</p>
Note, that the <a> element is converted in a way that it is always the topmost element. This is intentional so that none element will ever break an <a> element. See this, incorrect view/HTML string:
<p>
Foo <strong>bar</strong>
</p>
The generated view/HTML has two link elements next to each other, which is wrong.
We use priority property of view.AttributeElement to define which element should be on top of others. Most elements, like <strong> do not care about it and keep the default priority. However, <a> element has changed priority to guarantee a proper order in the view/HTML.
Complex inline elements and merging
So far we mostly discussed the simpler inline elements, i.e. elements which don't have attributes. Examples are <strong>, <em>. In contrary, <a> has additional attributes.
It is easy to come up with features that need to mark/style a part of a text but are custom enough so that simply using a tag is not enough. An example would be a font family feature. When used, it adds fontFamily attribute to a text, which is later converted to <span> element with an appropriate style attribute.
At this point, you need to ask what should happen if multiple such attributes are set on the same part of a text? Take this model example:
<paragraph>
<$text fontFamily="Tahoma" fontSize="big">Foo</$text>
</paragraph>
The above attributes convert as follow:
fontFamily="value" converts to <span style="font-family: value;">,
fontSize="value" converts to <span class="text-value">.
So, what kind of view/HTML could we expect?
<p>
<span style="font-family: Tahoma;">
<span class="text-big">Foo</span>
</span>
</p>
This, however, seems wrong. Why not have just one <span> element? Wouldn't it be better this way?
<p>
<span style="font-family: Tahoma;" class="text-big">Foo</span>
</p>
To solve situations like these, in CKEditor 5 conversion mechanism we, in fact, introduced a merging mechanism.
In the above scenario, we have two attributes that convert to <span>. When the first attribute (say, fontFamily is converted, there is no <span> in the view yet. So the <span> is added with the style attribute. However, when fontSize is converted, there is already <span> in the view. view.Writer recognizes this and checks whether those elements can be merged. The rules are three:
elements must have the same view.Element#name,
elements must have the same view.AttributeElement#priority,
neither element may have view.AttributeElement#id set.
We haven't discussed id property yet but, for simplicity reasons, I won't talk about it now. It is enough to say that it is important for some attribute elements to prevent merging them.
Adding another attribute to the link
At this point, it should be pretty clear how to add another attribute to <a> element.
All that needs to be done is defining a new model attribute (linkTarget or linkRel) and make it convert to <a> element with the desired (target="..." or rel="...") attribute. Then, it will be merged with the original <a href="..."> element.
Keep in mind that <a> element from the original CKEditor 5 link plugin has custom priority specified. This means that the element generated by the new plugin need to have the same priority specified to be properly merged.
Upcasting merged attribute elements
For now, we only discussed downcasting (i.e. converting from the model to the view). Now let's talk about upcasting (i.e. converting from the view to the model). Fortunately, it is easier than the previous part.
There are two "things" that can be upcasted - elements and attributes. No magic here - elements are elements (<p>, <a>, <strong>, etc.) and attributes are attributes (class="", href="", etc.).
Elements can be upcast to elements (<p> -> <paragraph>) or attributes (<strong> -> bold, <a> -> linkHref). Attributes can be upcast to attributes.
Our example clearly needs upcasting from an element to an attribute. Indeed, <a> element is converted to linkHref attribute and the linkHref attribute value is taken from href="" attribute of the <a> element.
Naturally, one would define the same conversion for their new linkTarget or linkRel attribute. However, there is a trap here. Each part of the view can be converted ("consumed") only once (this is also true for the model when downcasting).
What does it mean? Simply, if one feature already converted given element name or given element attribute, neither feature can also convert it. This way features can correctly overwrite each other. This also means that general-purpose converters can be introduced (for example, <div> can be converted to <paragraph> if no other feature recognized <div> as something that can be converted by that feature). This also helps to spot conflicting converters.
Back to our example. We cannot define two element-to-attribute converters that convert the same element (<a>) and expect them to work together at the same time. One will overwrite the other.
Since we don't want to change the original link plugin, we need to keep that converter as is. However, the upcast converter for the new plugin will be an attribute-to-attribute converter. Since that converter won't convert element (or rather, element name) it will work together with the original converter.
Code sample
Here is a code sample for a link target plugin. Below I will explain some parts of it.
import Plugin from '#ckeditor/ckeditor5-core/src/plugin';
import { downcastAttributeToElement } from '#ckeditor/ckeditor5-engine/src/conversion/downcast-converters';
import { upcastAttributeToAttribute } from '#ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
class LinkTarget extends Plugin {
init() {
const editor = this.editor;
editor.model.schema.extend( '$text', { allowAttributes: 'linkTarget' } );
editor.conversion.for( 'downcast' ).add( downcastAttributeToElement( {
model: 'linkTarget',
view: ( attributeValue, writer ) => {
return writer.createAttributeElement( 'a', { target: attributeValue }, { priority: 5 } );
},
converterPriority: 'low'
} ) );
editor.conversion.for( 'upcast' ).add( upcastAttributeToAttribute( {
view: {
name: 'a',
key: 'target'
},
model: 'linkTarget',
converterPriority: 'low'
} ) );
}
}
For such a long tutorial it surely is a small snippet. Hopefully, most of it is self-explanatory.
First, we expand Schema by defining a new attribute linkTarget that is allowed on text.
Then, we define downcast conversion. downcastAttributeToElement is used as we want to create <a target="..."> element that will be merged with the original <a> element. Keep in mind that the <a> element that is created here has the priority defined to 5, just as in the original link plugin.
The last step is upcast conversion. upcastAttributeToAttribute helper is used, as discussed earlier. In view configuration, it is specified that only target attribute of <a> element should be converted (name: 'a'). This does not mean that <a> element will be converted! This is only filtering configuration for the converter, so it won't convert target attribute of some other element.
Lastly, both converters are added with priority lower than the original converters to prevent any hypothetical problems.
The above sample works for me on the current master of ckeditor5-engine and ckeditor5-link.
Szymon Cofalik's answer is great, but not working anymore for at least the current CKE5 version (34.2.0).
The functions downcastAttributeToElement() and upcastAttributeToAttribute() are not exported anymore, so you have to use attributeToElement() and attributeToAttribute() from the Conversion API, which are available by default.
Updated code example:
import Plugin from '#ckeditor/ckeditor5-core/src/plugin';
class LinkTarget extends Plugin {
init() {
const editor = this.editor;
editor.model.schema.extend( '$text', { allowAttributes: 'linkTarget' } );
editor.conversion.for( 'downcast' ).attributeToElement( {
model: 'linkTarget',
view: ( attributeValue, writer ) => {
return writer.createAttributeElement( 'a', { target: attributeValue }, { priority: 5 } );
},
converterPriority: 'low'
} );
editor.conversion.for( 'upcast' ).attributeToAttribute( {
view: {
name: 'a',
key: 'target'
},
model: 'linkTarget',
converterPriority: 'low'
} );
}
}
as I came to the same problem in 2022, I founded this Answer very helpful, I wanted to add id attribute but didn't create my own plugin, I just edited the Link plugin in ckeditor5-build-classic package then I re-builded it.
1- in #module link/linkediting:
Allow link attribute on all inline nodes.
editor.model.schema.extend( '$text', { allowAttributes: ['linkHref', 'linkId'] } );
add conversion for upcast to conserve existed id attribute or create new one:
editor.conversion.for( 'upcast' ).attributeToAttribute( {
view: {
name: 'a'
},
model: {
key: 'linkId',
value: viewElement => {
let id = viewElement.getAttribute( 'id' );
if (id)
return id;
return 'id_'+Math.floor(Math.random() * 10000)
}
},
converterPriority: 'low'
} ) ;
add conversion for editingDowncast, to transform the Model into the view:
editor.conversion.for( 'editingDowncast' ).attributeToElement( {
model: 'linkId',
view: ( attributeValue, conversionApi ) => {
return conversionApi.writer.createAttributeElement( 'a', { id: attributeValue }, { priority: 5 } );
},
converterPriority: 'low'
} ) ;
add conversion for dataDowncast, to get the Attribute when getDate() is called:
editor.conversion.for( 'dataDowncast' )
.attributeToElement( {
model: 'linkId',
view: ( attributeValue, conversionApi ) => {
return conversionApi.writer.createAttributeElement( 'a', { id: attributeValue }, { priority: 5 } );
}
} ) ;
2- in #module link/linkcommand : to create the id, I just wanted a random string so I didn't made any new field in the form, and just added this line after the one that is responsible for linkHref Attribute
writer.setAttribute( 'linkId', 'id_'+Math.floor(Math.random() * 10000), range );

angular 2 spinner, slider: custom pipe not working

The following is the section of my angular component template that is not working:
<p-spinner id="yzDistance"
[min]="aRenderState.clipping.planes[0].min"
[max]="aRenderState.clipping.planes[0].max"
[step]="inputStep"
[(ngModel)]="yzDistance" (onChange)="moveClip(0)">
</p-spinner>
<input type="range" class="slider" type="range" name="yzDistance"
[min]="aRenderState.clipping.planes[0].min"
[max]="aRenderState.clipping.planes[0].max"
[step]="inputStep"
[ngModel]="yzDistance | decimalsPipe"
(ngModelChange)="yzDistance=$event"
(input)="moveClip(0)">
the spinner is working fine showing values formatted correctly e.g 2.009 4.765 -1.649 etc. (3 decimal places). When I move the slider that also has a step of 0.001 the spinner get updated but displays decimals with thousand separators e.g. 3.987,432 -1.34,092 etc. I have tried to correct the problem with the following custom pipe called decimalsPipe:
#Pipe({name: 'decimalsPipe'})
export class DecimalsPipe implements PipeTransform {
transform(value) {
value.toLocaleString('en-US', {
minimumFractionDigits: 0,
maximumFractionDigits: 3
});
}
}
#Component({
selector: 'myComponent',
templateUrl: './myComponent.html',
styleUrls: ['./myComponent.css']
})
export class myComponent { ...
it still showing the weird decimal formatting and it does not raise errors. Can you help me to sort this out?
Thank you, Dino
u do not return any value in transform , try to return the formatted value in the method transform :
transform(value) {
return value.toLocaleString('en-US', {
minimumFractionDigits: 0,
maximumFractionDigits: 3
});
}
I have accepted Med_Ali_Rachid answer even if it doesn't work as his answer point me in the right direction. The problem is the spinner that is not formatting the value returned by the pipe.
My solution was to hide the input area of the spinner leaving only the up and down arrows buttons visible. Then I have added a paragraph styled as an input to display the correct value.
<p>
{{yzDistance | decimalsPipe}}
</p>
<p-spinner id="yzDistance"
[min]="aRenderState.clipping.planes[0].min"
[max]="aRenderState.clipping.planes[0].max"
[step]="inputStep"
[(ngModel)]="yzDistance" (onChange)="moveClip(0)">
</p-spinner>

Integrate bootstrap 3 typeahead and tags input with objects as tags

I am having trouble integrating bootstrap 3 typeahead with tags input but with objects as tags. It works if I use only typeahead on input field, but if I integrate it with tags input then it doesn't work and I don't even get any errors which is really frustrating. Here's my code:
var places = [{name: "New York"}, {name: "Los Angeles"}];
//this works
$('input').typeahead({
source: places
});
//this doesn't
$('input').tagsinput({
freeInput: false,
confirmKeys: [44],
typeahead: {
source: places
}
});
Am I doing something wrong or is this a bug?
If anyone has a working example of this I'd really appreciate some help, it can be typeahead.js instead of bootstrap 3 typeahead, I tried to use that as well and it works but then I have a problem where if I choose a suggested option from typeahead clicking enter submits the whole form instead of just accepting that option as a tag.
You should attach the typeahead to tagsinput via the typeahead option! This is much easier (and what the docs suggests). Then, if you map() places to an array of strings it works :
$('.tagsinput-typeahead').tagsinput({
// other tagsinput options here
typeahead: {
source: places.map(function(item) { return item.name }),
afterSelect: function() {
this.$element[0].value = '';
}
}
})
demo -> http://jsfiddle.net/gm3a1s9k/1/
Notice the afterSelect(), it is needed in order to clear the input.

Dijit multiple widgets not parsed when row deleted with dGrid (dgrid.io)

I use dGrid 0.4.1-dev (dgrid.io) with a custom formatter (I have tried with custom renderer) to return raw HTMLthat contains Dijit widgets , during the page load, all widgets has been parsed correctly but it's totally broken when sort, paginate or scroll (OnDemandGrid).
In my example, I use Tooltip widget on icons (declarative mode, see bottom "Returned HTML for the cell").
On page load:
After scroll,sort, paginate...:
Returned HTML for the cell (by the formatter):
<div data-dojo-type="dijit/Tooltip" id="comxDynElement_101_dijit_1" data-dojo-props="connectId:'comxDynElement_101',position:['above']">Dupliquer cette facture</div><i class="fa fa-clone"></i>
<div data-dojo-type="dijit/Tooltip" id="comxDynElement_102_dijit_1" data-dojo-props="connectId:'comxDynElement_102',position:['above']">Télécharger cette facture</div><a id="comxDynElement_102" href="..."><i class="fa fa-file-pdf-o"></i></a>
<div data-dojo-type="dijit/Tooltip" id="comxDynElement_103_dijit_1" data-dojo-props="connectId:'comxDynElement_103',position:['above']">Voir cette facture</div><a id="comxDynElement_103" href="..."><i class="fa fa-eye"></i></a>
My formatter:
function (item) {
return item;
}
The widgets are being build and started on page load by the Dojo Parser. When new declarative html is added to your page after the page is already loaded, these widgets are not automatically parsed. You need to do that manually.
For example, it should be possible to call the parser manually in a renderRow function of your grid. An example can be found here.
So the following code could work:
require([
'dgrid/OnDemandList',
'dojo/parser'
], function (OnDemandList,parser) {
var list = new OnDemandList({
collection: myStore, // a dstore collection
renderRow: function (object, options) {
var div = document.createElement('div');
div.appendChild(document.createTextNode(object.myField));
// only 'parse' new row, not whole document again.
parser.parse(div);
return div;
}
});
});

How to get multiple textareas with TinyMCE on same page dynamically in an MVC 4 app page?

I have what I think is a simple scenario where I have to generate multiple textareas with RTE capability. I am using TinyMce which works marvelously if I only have one textarea, but the others don't. I have created a simple example MVC 4 app to try to get it all working before migrating my new knowledge to the real app. There are other items on this page that are all editable so it appears that the problem might stem from the html helper. Or from the fact that the resultant html shows that all three textareas have the same id. However, since the code doesn't obviously reference the id I didn't think I would matter. Anyone know for sure?
I have a simple model:
TextModel text = new TextModel();
text.P1 = "This is an editable element.";
I have included TinyMce in my BundleConfig file, then in my _Layout. Then I have a strongly typed view.
#model TinyMCE_lite.Models.TextModel
And a script section to expand my textareas on focus:
<script>
$(window).load(function () {
$('textarea.expand').focus(function () {
$(this).addClass("expanding")
$(this).animate({
height: "10em"
}, 200);
});
$('textarea.expand').blur(function () {
$(this).animate({
height: "28px"
}, 100);
$(this).removeClass("expanding")
});
});
Then I crank out three in a loop:
#using (Html.BeginForm("Index", "Home", FormMethod.Post))
{
<fieldset>
<h1 class="editable">Editable header</h1>
#for (int count = 0; count < 3; count++ )
{
int index = count + 1;
<h3>#index</h3>
<p class="editable">#Html.DisplayFor(model => model.P1)</p>
#Html.TextAreaFor(model => model.P2, 0,0, new { #class = "expand" })
}
<div style="margin-top: 25px;"><input type="submit" value="Save" /></div>
</fieldset>
}
The first one acts as expected, showing the editor elements, but not the others. All three expand as expect. Hopefully I have overlooked something simple. Any ideas?
Wow! Turns out it was the identical ids. All I had to do was create unique ids and it works nicely.
I changed this:
#Html.TextAreaFor(model => model.P2, 0,0, new { #class = "expand" })
to this:
#Html.TextAreaFor(model => model.P2, 0,0, new { #class = "expand", id = #count })
Hopefully this will prevent someone else from hours of agony trying to figure this out.