I need a placeholder/variable that takes name, defaultValue, tooltip/description. I created a plugin and it is working fine in the editor/create mode. When placeholder is created, it is adding the following tags to source
<var class="cke_placeholder" name="varName" title="varToolTip">[[varDefaultValue]]</var>
Image that depicts create & edit mode differences
When I save the html content with placehoder in db and trying to load it back to ckeditor, I am not able to get the + symbol and hence not able to launch the editor.
Here is my ckeditor/plugins/var/plguin.js
'use strict';
( function() {
CKEDITOR.plugins.add( 'var', {
requires: 'widget,dialog',
icons: 'var', // %REMOVE_LINE_CORE%
hidpi: true, // %REMOVE_LINE_CORE%
onLoad: function() {
CKEDITOR.dialog.add( 'var', this.path + 'dialogs/var.js' );
},
init: function( editor ) {
this.registerWidget( editor );
editor.ui.addButton && editor.ui.addButton( 'Var', {
label: 'Create Variable',
command: 'var',
toolbar: 'insert',
icon: 'var'
} );
},
registerWidget: function(editor){
var that = this;
// Put ur init code here.
editor.widgets.add( 'var', {
// Widget code.
dialog: 'var',
pathName: 'var',
// We need to have wrapping element, otherwise there are issues in
// add dialog.
template: '<var class="cke_placeholder">[[]]</var>',
downcast: function() {
return new CKEDITOR.htmlParser.text( '<var class="cke_placeholder" name="'+this.data.name+'" title="'+this.data.description+'">[[' + this.data.defaultValue + ']]</var>' );
},
init: function() {
this.setData( 'defaultValue', this.element.getText().slice( 2, -2 ) );
this.setData( 'name', this.element.getAttribute("name") );
this.setData( 'description', this.element.getAttribute("title") );
},
data: function() {
this.element.setText( '[[' + this.data.defaultValue + ']]' );
this.element.setAttribute('name', this.data.name );
this.element.setAttribute('title', this.data.description );
}
} );
},
afterInit: function( editor ) {
this.registerWidget( editor );
/*var placeholderReplaceRegex = /\[\[([^\[\]])+\]\]/g;
editor.dataProcessor.dataFilter.addRules( {
text: function( text, node ) {
var dtd = node.parent && CKEDITOR.dtd[ node.parent.name ];
// Skip the case when placeholder is in elements like <title> or <textarea>
// but upcast placeholder in custom elements (no DTD).
if ( dtd && !dtd.span )
return;
return text.replace( placeholderReplaceRegex, function( match ) {
// Creating widget code.
var widgetWrapper = null,
innerElement = new CKEDITOR.htmlParser.element( 'span', {
'class': 'cke_placeholder'
} );
// Adds placeholder identifier as innertext.
innerElement.add( new CKEDITOR.htmlParser.text( match ) );
widgetWrapper = editor.widgets.wrapElement( innerElement, 'placeholder' );
// Return outerhtml of widget wrapper so it will be placed
// as replacement.
return widgetWrapper.getOuterHtml();
} );
}
} );*/
}
} );
} )();
Here is my ckeditor/plugins/var/dialogs/var.js
'use strict';
CKEDITOR.dialog.add( 'var', function( editor ) {
//var lang = editor.lang.var,
//generalLabel = editor.lang.common.generalTab,
var generalLabel = 'General',
validRegex = /^[^\[\]<>]+$/,
emptyOrInvalid = ' can not be empty. It can not contain any of following characters: [, ], <, >',
invalid = ' can not contain any of following characters: [, ], <, >';
return {
title: 'Variable properties',
minWidth: 300,
minHeight: 80,
contents: [
{
id: 'info',
label: generalLabel,
title: generalLabel,
elements: [
// Dialog window UI elements.
{
id: 'name',
type: 'text',
style: 'width: 100%;',
label: 'Name',
'default': '',
required: true,
validate: CKEDITOR.dialog.validate.regex( validRegex, 'name'+emptyOrInvalid ),
setup: function( widget ) {
this.setValue( widget.data.name );
},
commit: function( widget ) {
widget.setData( 'name', this.getValue() );
}
},
{
id: 'defaultValue',
type: 'text',
style: 'width: 100%;',
label: 'Default Value',
'default': '',
required: true,
validate: CKEDITOR.dialog.validate.regex( validRegex, 'Default Value'+emptyOrInvalid ),
setup: function( widget ) {
this.setValue( widget.data.defaultValue );
},
commit: function( widget ) {
widget.setData( 'defaultValue', this.getValue() );
}
},
{
id: 'description',
type: 'text',
style: 'width: 100%;',
label: 'Description',
'default': '',
required: true,
validate: CKEDITOR.dialog.validate.regex( validRegex, 'Description'+invalid ),
setup: function( widget ) {
this.setValue( widget.data.description );
},
commit: function( widget ) {
widget.setData( 'description', this.getValue() );
}
}
]
}
]
};
} );
Related
Is there anyway to force the CKEDITOR to just be plain text when I enter this TextEnter it sends out this <p>TextEnter</p>\n. I wanted to see if any of the config setting can help me with that.
CKEDITORInitializer() {
if ((<any>window).CKEDITOR.instances.indicationElementId)
(<any>window).CKEDITOR.instances.indicationElementId.destroy();
(<any>window).CKEDITOR.disableAutoInline = true;
(<any>window).CKEDITOR.config.forcePasteAsPlainText = true;
(<any>window).CKEDITOR.config.autoParagraph = false;
(<any>window).CKEDITOR.replace("indicationElementId", {
height: '60',
toolbar: [
{ name: 'insert', items: ['SpecialChar'] },
],
specialChars: ['©', '®', '–', '¾', '≥', '≤'],
});
(<any>window).CKEDITOR.instances["indicationElementId"].on("change", () => {
this.ngZone.run(() => {
this.item.ValueName = this.getContent();
this.indicationContentChanged.next(null);
});
});
}
I am currently trying to write a plugin for the CKEditor 5 to support automatic translations. I was able to find out how to write plugins and how to create dropdowns in the documentation.
But in the documentation there is no mention (or I missed it) how to be informed about a click on the values:
There is an Execute Handler for the button that opens the dropdown, but how do I register a listener for a click on one of the values?
Can I assign an id or similar to my items to recognize the click on the right element of the dropdown?
Here's the code that I was able to build based on the documentation:
class Translation extends Plugin {
init() {
this.editor.ui.componentFactory.add('translate', (locale) => {
const dropdownView = createDropdown(locale);
dropdownView.buttonView.set({
icon: languageIcon,
label: 'Translate',
tooltip: true,
});
const items = new Collection();
items.add({
id: 'en', // how to assign id ???
type: 'button',
model: new Model({
withText: true,
label: 'English'
}),
});
items.add({
id: 'es', // how to assign id ???
type: 'button',
model: new Model({
withText: true,
label: 'Spanish'
}),
});
addListToDropdown(dropdownView, items);
// callback for click on item ????
dropdownView.on('click', (event) => {
console.log('click', event);
});
return dropdownView;
});
}
}
You can use DropdownView.on() method to listen to the execute event.
And, use EventInfo.source property to get the object that is clicked and then use its property e.g. id or label to identify it.
For example:
const items = new Collection();
items.add( {
type: 'button',
model: new Model({
id: 'en', // id
withText: true,
label: 'English',
})
} );
items.add( {
type: 'button',
model: new Model({
id: 'es', // id
withText: true,
label: 'Spanish'
})
} );
addListToDropdown(dropdownView, items);
dropdownView.on('execute', (eventInfo) => {
const { id, label } = eventInfo.source;
if ( id === 'en' ) {
console.log('Object (en):', label);
} else if ( id === 'es' ) {
console.log('Object (es):', label);
}
});
Here's the complete working example with ClassicEditor (tested):
import ClassicEditor from '#ckeditor/ckeditor5-editor-classic/src/classiceditor';
import Essentials from '#ckeditor/ckeditor5-essentials/src/essentials';
import Model from '#ckeditor/ckeditor5-ui/src/model';
import Collection from '#ckeditor/ckeditor5-utils/src/collection';
import { createDropdown, addListToDropdown } from '#ckeditor/ckeditor5-ui/src/dropdown/utils';
import Plugin from '#ckeditor/ckeditor5-core/src/plugin';
const Translate = 'translate';
class Translation extends Plugin {
init() {
console.log('Translation initialized!');
this.editor.ui.componentFactory.add(Translate, (locale) => {
const dropdownView = createDropdown(locale);
dropdownView.buttonView.set({
label: 'Translate',
withText: true,
});
const items = new Collection();
items.add( {
type: 'button',
model: new Model({
id: 'en',
withText: true,
label: 'English',
})
} );
items.add( {
type: 'button',
model: new Model({
id: 'es',
withText: true,
label: 'Spanish'
})
} );
addListToDropdown(dropdownView, items);
dropdownView.on('execute', (eventInfo) => {
const { id, label } = eventInfo.source;
if ( id === 'en' ) {
console.log('Object (en):', label);
} else if ( id === 'es' ) {
console.log('Object (es):', label);
}
});
return dropdownView;
});
}
}
ClassicEditor
.create( document.querySelector( '#editor' ), {
plugins: [ Essentials, Translation ],
toolbar: [ Translate ]
} )
.then( editor => {
console.log( 'Editor was initialized', editor );
} )
.catch( error => {
console.error( error.stack );
} );
Console output after clicking both items:
Object (en): English
Object (es): Spanish
You can use execute. it will fire an event when the toolbar button or list item is executed.for listView It fires when a child of some ListItemView fired execute. for toolbarView It fires when one of the buttons has been executed. execute will return EventInfo object when event fires. Also, there is off() and stop() method to de-register the event listener.
Note: Only supported when dropdown has list view added using addListToDropdown or addToolbarToDropdown.
Here is the snippet, give it a try.
this.listenTo( dropdownView, 'execute', eventInfo => {
console.log(eventInfo.source);
} );
----------------------------------------------------------- OR ------------------------------------------------------------------
dropdownView.on('execute', eventInfo => {
console.log(eventInfo.source);
} );
I have developed a few plugins for ckeditor which work by adding span tag with style attribute.
Letter Spacing
Line Height
Text Transform
They are working fine when I make changes in editor but, when I initialise the editor with <p><span style="line-height:3;letter-spacing:18px;text-transform:uppercase;font-family:Ubuntu, Arial, sans-serif;font-size:12px;">safasfasfas</span></p> only text-transform, font-family and font-size is working but rest of 2 plugins/style attributes are not working.
Is there any idea, what I am doing wrong?
Edit:
I have conversion like below
editor.conversion.for( 'downcast' )
.add( downcastAttributeToElement( {
model: {
key: 'letterSpacing',
name: '$text'
},
view: ( modelAttributeValue, viewWriter ) => {
return viewWriter.createAttributeElement( 'span', { style: 'letter-spacing:' + modelAttributeValue + 'px' } );
}
} ) );
editor.conversion.for( 'upcast' )
.add( upcastElementToAttribute( {
view: {
name: 'span'
},
model: {
key: 'letterSpacing',
value: viewElement => {
const letterSpacing = viewElement.getStyle( 'letter-spacing' );
if (letterSpacing === undefined) {
return null;
}
return letterSpacing.substr( 0, letterSpacing.length - 2 );
}
}
} ) );
I was able to fix this issue by adding below code in conversion.
styles: {
'letter-spacing': /[\S]+/
}
Now conversion becomes like
editor.conversion.for( 'downcast' )
.add( downcastAttributeToElement( {
model: {
key: 'letterSpacing',
name: '$text'
},
view: ( modelAttributeValue, viewWriter ) => {
return viewWriter.createAttributeElement( 'span', { style: 'letter-spacing:' + modelAttributeValue + 'px' } );
}
} ) );
editor.conversion.for( 'upcast' )
.add( upcastElementToAttribute( {
view: {
name: 'span',
styles: {
'letter-spacing': /[\S]+/
}
},
model: {
key: 'letterSpacing',
value: viewElement => {
const letterSpacing = viewElement.getStyle( 'letter-spacing' );
if (letterSpacing === undefined) {
return null;
}
return letterSpacing.substr( 0, letterSpacing.length - 2 );
}
}
} ) );
I am able to get tree structure for the user stories but want it same for defects also which are related to particular user story so that at a singe screen I can see both user stories and the related defects.
You may use features: [{ftype:'groupingsummary'}] of ExtJS to group defects by user stories and even summarize by some other field, in the code below by PlanEstimate. To group defects by user story Requirement attribute on defect is used, which points to the related story. In this example defects are filtered by Iteration.
Ext.define('CustomApp', {
extend: 'Rally.app.TimeboxScopedApp',
componentCls: 'app',
scopeType: 'iteration',
comboboxConfig: {
fieldLabel: 'Select Iteration:',
labelWidth: 100
},
onScopeChange: function() {
this.makeStore();
},
makeStore: function() {
var filter = Ext.create('Rally.data.wsapi.Filter', {
property: 'Requirement',
operator: '!=',
value: null
});
filter= filter.and(this.getContext().getTimeboxScope().getQueryFilter());
filter.toString();
Ext.create('Rally.data.wsapi.Store', {
model: 'Defect',
fetch: ['ObjectID', 'FormattedID', 'Name', 'State', 'Requirement', 'PlanEstimate'],
autoLoad: true,
filters: [filter],
listeners: {
load: this.onDataLoaded,
scope: this
}
});
},
onDataLoaded: function(store, records){
if (records.length === 0) {
this.notifyNoDefects();
}
else{
if (this.notifier) {
this.notifier.destroy();
}
var that = this;
var promises = [];
_.each(records, function(defect) {
promises.push(this.getStory(defect, this));
},this);
Deft.Promise.all(promises).then({
success: function(results) {
that.defects = results;
that.makeGrid();
}
});
}
},
getStory: function(defect, scope) {
var deferred = Ext.create('Deft.Deferred');
var that = scope;
var storyOid = defect.get('Requirement').ObjectID;
Rally.data.ModelFactory.getModel({
type: 'HierarchicalRequirement',
scope: this,
success: function(model, operation) {
fetch: ['FormattedID','ScheduleState'],
model.load(storyOid, {
scope: this,
success: function(record, operation) {
var storyScheduleState = record.get('ScheduleState');
var storyFid = record.get('FormattedID');
var defectRef = defect.get('_ref');
var defectOid = defect.get('ObjectID');
var defectFid = defect.get('FormattedID');
var defectPlanEstimate = defect.get('PlanEstimate');
var defectName = defect.get('Name');
var defectState = defect.get('State');
var story = defect.get('Requirement');
result = {
"_ref" : defectRef,
"ObjectID" : defectOid,
"FormattedID" : defectFid,
"Name" : defectName,
"PlanEstimate" : defectPlanEstimate,
"State" : defectState,
"Requirement" : story,
"StoryState" : storyScheduleState,
"StoryID" : storyFid
};
deferred.resolve(result);
}
});
}
});
return deferred;
},
makeGrid: function() {
var that = this;
if (this.grid) {
this.grid.destroy();
}
var gridStore = Ext.create('Rally.data.custom.Store', {
data: that.defects,
groupField: 'StoryID',
pageSize: 1000,
});
this.grid = Ext.create('Rally.ui.grid.Grid', {
itemId: 'defectGrid',
store: gridStore,
features: [{ftype:'groupingsummary'}],
minHeight: 500,
columnCfgs: [
{
text: 'Formatted ID', dataIndex: 'FormattedID', xtype: 'templatecolumn',
tpl: Ext.create('Rally.ui.renderer.template.FormattedIDTemplate')
},
{
text: 'Name', dataIndex: 'Name',
},
{
text: 'State', dataIndex: 'State',
summaryRenderer: function() {
return "PlanEstimate Total";
}
},
{
text: 'PlanEstimate', dataIndex: 'PlanEstimate',
summaryType: 'sum'
},
{
text: 'Story', dataIndex: 'Story',
renderer: function(val, meta, record) {
return '' + record.get('Requirement').FormattedID + '';
}
},
{
text: 'Story Schedule State', dataIndex: 'StoryState',
}
]
});
this.add(this.grid);
this.grid.reconfigure(gridStore);
},
notifyNoDefects: function() {
if (this.grid) {
this.grid.destroy();
}
if (this.notifier) {
this.notifier.destroy();
}
this.notifier = Ext.create('Ext.Container',{
xtype: 'container',
itemId: 'notifyContainer',
html: "No Defects found matching selection."
});
this.add( this.notifier);
}
});
By default, CKEditor's enter key behavior is adding a <p> tag and starting a new paragraph. But that behavior can be modified in the configuration with a .EnterMode parameter.
I'm wondering if there is a way to change Enter key behavior in the runtime, by putting a button into its tool bar, to switch between <p> and <br> (single line vs double line), just like as in Word.
Any ideas on how to do this?
Save the following to editor_dir/plugins/entermode/plugin.js:
'use strict';
(function() {
CKEDITOR.plugins.add( 'entermode', {
icons: 'entermode',
init: function( editor ) {
var enterP = new enterModeCommand( editor, CKEDITOR.ENTER_P );
var enterBR = new enterModeCommand( editor, CKEDITOR.ENTER_BR );
editor.addCommand( 'entermodep', enterP );
editor.addCommand( 'entermodebr', enterBR );
if ( editor.ui.addButton ) {
editor.ui.addButton( 'EnterP', {
label: 'Enter mode P',
command: 'entermodep',
toolbar: 'paragraph,10'
});
editor.ui.addButton( 'EnterBR', {
label: 'Enter mode BR',
command: 'entermodebr',
toolbar: 'paragraph,20'
});
}
editor.on( 'dataReady', function() {
refreshButtons( editor );
});
}
});
function refreshButtons( editor ) {
editor.getCommand( 'entermodep' ).setState(
editor.config.enterMode == CKEDITOR.ENTER_P ? CKEDITOR.TRISTATE_ON : CKEDITOR.TRISTATE_OFF );
editor.getCommand( 'entermodebr' ).setState(
editor.config.enterMode == CKEDITOR.ENTER_BR ? CKEDITOR.TRISTATE_ON : CKEDITOR.TRISTATE_OFF );
}
function enterModeCommand( editor, mode ) {
this.mode = mode;
}
enterModeCommand.prototype = {
exec: function( editor ) {
editor.config.enterMode = this.mode;
refreshButtons( editor );
},
refresh: function( editor, path ) {
refreshButtons( editor );
}
};
})();
Now add the following to toolbar.css in your skin file:
.cke_button__enterp_icon, .cke_button__enterbr_icon { display: none !important; }
.cke_button__enterp_label, .cke_button__enterbr_label { display: inline !important; }
...or paint some icons, if you want.
Enable the plugin when running the editor:
CKEDITOR.replace( 'id', { extraPlugins: 'entermode' } );
Now you can control config.enterMode from the toolbar.