Skip to content

Commit

Permalink
Merge pull request #8812 from ckeditor/i/8789
Browse files Browse the repository at this point in the history
Fix (html-embed): Pasting HTML embed widget from the clipboard will not clear its content anymore. Closes #8789.

Feature (engine): New property `DataController#htmlProcessor` is initialized with `HtmlDataProcessor` and assigned to `DataController#processor` by default.

Internal (clipboard): Plugin now uses `HtmlDataProcessor` instance from the `DataController`.

Internal (editor-balloon, editor-classic, editor-inline, editor-decoupled): The `editor.processor` property is automatically initialized with `HtmlDataProcessor` now.
  • Loading branch information
niegowski committed Jan 13, 2021
2 parents 6e40289 + 11ff537 commit 2025f40
Show file tree
Hide file tree
Showing 10 changed files with 136 additions and 45 deletions.
13 changes: 2 additions & 11 deletions packages/ckeditor5-clipboard/src/clipboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import plainTextToHtml from './utils/plaintexttohtml';
import normalizeClipboardHtml from './utils/normalizeclipboarddata';
import viewToPlainText from './utils/viewtoplaintext.js';

import HtmlDataProcessor from '@ckeditor/ckeditor5-engine/src/dataprocessor/htmldataprocessor';
import EventInfo from '@ckeditor/ckeditor5-utils/src/eventinfo';

/**
Expand Down Expand Up @@ -52,14 +51,6 @@ export default class Clipboard extends Plugin {
const view = editor.editing.view;
const viewDocument = view.document;

/**
* Data processor used to convert pasted HTML to a view structure.
*
* @private
* @member {module:engine/dataprocessor/htmldataprocessor~HtmlDataProcessor} #_htmlDataProcessor
*/
this._htmlDataProcessor = new HtmlDataProcessor( viewDocument );

view.addObserver( ClipboardObserver );

// The clipboard paste pipeline.
Expand All @@ -82,7 +73,7 @@ export default class Clipboard extends Plugin {
content = plainTextToHtml( dataTransfer.getData( 'text/plain' ) );
}

content = this._htmlDataProcessor.toView( content );
content = this.editor.data.htmlProcessor.toView( content );

const eventInfo = new EventInfo( this, 'inputTransformation' );
this.fire( eventInfo, {
Expand Down Expand Up @@ -175,7 +166,7 @@ export default class Clipboard extends Plugin {

this.listenTo( viewDocument, 'clipboardOutput', ( evt, data ) => {
if ( !data.content.isEmpty ) {
data.dataTransfer.setData( 'text/html', this._htmlDataProcessor.toData( data.content ) );
data.dataTransfer.setData( 'text/html', this.editor.data.htmlProcessor.toData( data.content ) );
data.dataTransfer.setData( 'text/plain', viewToPlainText( data.content ) );
}

Expand Down
3 changes: 0 additions & 3 deletions packages/ckeditor5-editor-balloon/src/ballooneditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
*/

import Editor from '@ckeditor/ckeditor5-core/src/editor/editor';
import HtmlDataProcessor from '@ckeditor/ckeditor5-engine/src/dataprocessor/htmldataprocessor';
import BalloonToolbar from '@ckeditor/ckeditor5-ui/src/toolbar/balloon/balloontoolbar';
import BalloonEditorUI from './ballooneditorui';
import BalloonEditorUIView from './ballooneditoruiview';
Expand Down Expand Up @@ -77,8 +76,6 @@ export default class BalloonEditor extends Editor {

this.config.define( 'balloonToolbar', this.config.get( 'toolbar' ) );

this.data.processor = new HtmlDataProcessor( this.data.viewDocument );

this.model.document.createRoot();

const view = new BalloonEditorUIView( this.locale, this.editing.view, this.sourceElement );
Expand Down
3 changes: 0 additions & 3 deletions packages/ckeditor5-editor-classic/src/classiceditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import Editor from '@ckeditor/ckeditor5-core/src/editor/editor';
import DataApiMixin from '@ckeditor/ckeditor5-core/src/editor/utils/dataapimixin';
import ElementApiMixin from '@ckeditor/ckeditor5-core/src/editor/utils/elementapimixin';
import attachToForm from '@ckeditor/ckeditor5-core/src/editor/utils/attachtoform';
import HtmlDataProcessor from '@ckeditor/ckeditor5-engine/src/dataprocessor/htmldataprocessor';
import ClassicEditorUI from './classiceditorui';
import ClassicEditorUIView from './classiceditoruiview';
import getDataFromElement from '@ckeditor/ckeditor5-utils/src/dom/getdatafromelement';
Expand Down Expand Up @@ -66,8 +65,6 @@ export default class ClassicEditor extends Editor {
this.sourceElement = sourceElementOrData;
}

this.data.processor = new HtmlDataProcessor( this.data.viewDocument );

this.model.document.createRoot();

const shouldToolbarGroupWhenFull = !this.config.get( 'toolbar.shouldNotGroupWhenFull' );
Expand Down
3 changes: 0 additions & 3 deletions packages/ckeditor5-editor-decoupled/src/decouplededitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@

import Editor from '@ckeditor/ckeditor5-core/src/editor/editor';
import DataApiMixin from '@ckeditor/ckeditor5-core/src/editor/utils/dataapimixin';
import HtmlDataProcessor from '@ckeditor/ckeditor5-engine/src/dataprocessor/htmldataprocessor';
import DecoupledEditorUI from './decouplededitorui';
import DecoupledEditorUIView from './decouplededitoruiview';
import getDataFromElement from '@ckeditor/ckeditor5-utils/src/dom/getdatafromelement';
Expand Down Expand Up @@ -72,8 +71,6 @@ export default class DecoupledEditor extends Editor {
secureSourceElement( this );
}

this.data.processor = new HtmlDataProcessor( this.data.viewDocument );

this.model.document.createRoot();

const shouldToolbarGroupWhenFull = !this.config.get( 'toolbar.shouldNotGroupWhenFull' );
Expand Down
3 changes: 0 additions & 3 deletions packages/ckeditor5-editor-inline/src/inlineeditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import Editor from '@ckeditor/ckeditor5-core/src/editor/editor';
import DataApiMixin from '@ckeditor/ckeditor5-core/src/editor/utils/dataapimixin';
import ElementApiMixin from '@ckeditor/ckeditor5-core/src/editor/utils/elementapimixin';
import attachToForm from '@ckeditor/ckeditor5-core/src/editor/utils/attachtoform';
import HtmlDataProcessor from '@ckeditor/ckeditor5-engine/src/dataprocessor/htmldataprocessor';
import InlineEditorUI from './inlineeditorui';
import InlineEditorUIView from './inlineeditoruiview';
import setDataInElement from '@ckeditor/ckeditor5-utils/src/dom/setdatainelement';
Expand Down Expand Up @@ -64,8 +63,6 @@ export default class InlineEditor extends Editor {
constructor( sourceElementOrData, config ) {
super( config );

this.data.processor = new HtmlDataProcessor( this.data.viewDocument );

this.model.document.createRoot();

if ( isElement( sourceElementOrData ) ) {
Expand Down
60 changes: 45 additions & 15 deletions packages/ckeditor5-engine/src/controller/datacontroller.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import ViewDowncastWriter from '../view/downcastwriter';

import ModelRange from '../model/range';
import { autoParagraphEmptyRoots } from '../model/utils/autoparagraphing';
import HtmlDataProcessor from '../dataprocessor/htmldataprocessor';

/**
* Controller for the data pipeline. The data pipeline controls how data is retrieved from the document
Expand Down Expand Up @@ -59,21 +60,6 @@ export default class DataController {
*/
this.model = model;

/**
* Styles processor used during the conversion.
*
* @readonly
* @member {module:engine/view/stylesmap~StylesProcessor}
*/
this.stylesProcessor = stylesProcessor;

/**
* Data processor used during the conversion.
*
* @member {module:engine/dataprocessor/dataprocessor~DataProcessor} #processor
*/
this.processor = undefined;

/**
* Mapper used for the conversion. It has no permanent bindings, because they are created when getting data and
* cleared directly after the data are converted. However, the mapper is defined as a class property, because
Expand Down Expand Up @@ -114,6 +100,30 @@ export default class DataController {
*/
this.viewDocument = new ViewDocument( stylesProcessor );

/**
* Styles processor used during the conversion.
*
* @readonly
* @member {module:engine/view/stylesmap~StylesProcessor}
*/
this.stylesProcessor = stylesProcessor;

/**
* Data processor used specifically for HTML conversion.
*
* @readonly
* @member {module:engine/dataprocessor/htmldataprocessor~HtmlDataProcessor} #htmlProcessor
*/
this.htmlProcessor = new HtmlDataProcessor( this.viewDocument );

/**
* Data processor used during the conversion.
* Same instance as {@link #htmlProcessor} by default. Can be replaced at run time to handle different format, e.g. XML or Markdown.
*
* @member {module:engine/dataprocessor/dataprocessor~DataProcessor} #processor
*/
this.processor = this.htmlProcessor;

/**
* The view downcast writer just for data conversion purposes, i.e. to modify
* the {@link #viewDocument}.
Expand Down Expand Up @@ -431,6 +441,26 @@ export default class DataController {
callback( this.stylesProcessor );
}

/**
* Registers a {@link module:engine/view/matcher~MatcherPattern} on {@link #htmlProcessor htmlProcessor}
* and {@link #processor processor} for view elements whose content should be treated as a raw data
* and not processed during conversion from DOM to view elements.
*
* The raw data can be later accessed by {@link module:engine/view/element~Element#getCustomProperty view element custom property}
* `"$rawContent"`.
*
* @param {module:engine/view/matcher~MatcherPattern} pattern Pattern matching all view elements whose content should
* be treated as a raw data.
*/
registerRawContentMatcher( pattern ) {
// No need to register the pattern if both `htmlProcessor` and `processor` are the same instances.
if ( this.processor && this.processor !== this.htmlProcessor ) {
this.processor.registerRawContentMatcher( pattern );
}

this.htmlProcessor.registerRawContentMatcher( pattern );
}

/**
* Removes all event listeners set by the DataController.
*/
Expand Down
55 changes: 50 additions & 5 deletions packages/ckeditor5-engine/tests/controller/datacontroller.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_uti
import { StylesProcessor } from '../../src/view/stylesmap';

describe( 'DataController', () => {
let model, modelDocument, htmlDataProcessor, data, schema, upcastHelpers, downcastHelpers, viewDocument;
let model, modelDocument, data, schema, upcastHelpers, downcastHelpers, viewDocument;

beforeEach( () => {
const stylesProcessor = new StylesProcessor();
Expand All @@ -39,11 +39,7 @@ describe( 'DataController', () => {
schema.register( '$title', { inheritAllFrom: '$root' } );

viewDocument = new ViewDocument( stylesProcessor );
htmlDataProcessor = new HtmlDataProcessor( viewDocument );

data = new DataController( model, stylesProcessor );
data.processor = htmlDataProcessor;

upcastHelpers = new UpcastHelpers( [ data.upcastDispatcher ] );
downcastHelpers = new DowncastHelpers( [ data.downcastDispatcher ] );
} );
Expand All @@ -63,6 +59,20 @@ describe( 'DataController', () => {

expect( data.viewDocument ).to.be.instanceOf( ViewDocument );
} );

it( 'should create #htmlProcessor property', () => {
const stylesProcessor = new StylesProcessor();
const data = new DataController( model, stylesProcessor );

expect( data.htmlProcessor ).to.be.instanceOf( HtmlDataProcessor );
} );

it( 'should assign #htmlProcessor property to the #processor property', () => {
const stylesProcessor = new StylesProcessor();
const data = new DataController( model, stylesProcessor );

expect( data.htmlProcessor ).to.equal( data.processor );
} );
} );

describe( 'parse()', () => {
Expand Down Expand Up @@ -738,4 +748,39 @@ describe( 'DataController', () => {
sinon.assert.calledWithExactly( spy, stylesProcessor );
} );
} );

describe( 'registerRawContentMatcher()', () => {
it( 'should not register matcher twice for one instance of data processor', () => {
const stylesProcessor = new StylesProcessor();
const data = new DataController( model, stylesProcessor );

const spy = sinon.spy();

data.processor.registerRawContentMatcher = spy;

data.registerRawContentMatcher( 'div' );

sinon.assert.calledOnce( spy );
sinon.assert.calledWithExactly( spy, 'div' );
} );

it( 'should register matcher on both of data processor instances', () => {
const stylesProcessor = new StylesProcessor();
const data = new DataController( model, stylesProcessor );
data.processor = new HtmlDataProcessor( viewDocument );

const spyProcessor = sinon.spy();
const spyHtmlProcessor = sinon.spy();

data.processor.registerRawContentMatcher = spyProcessor;
data.htmlProcessor.registerRawContentMatcher = spyHtmlProcessor;

data.registerRawContentMatcher( 'div' );

sinon.assert.calledOnce( spyProcessor );
sinon.assert.calledWithExactly( spyProcessor, 'div' );
sinon.assert.calledOnce( spyHtmlProcessor );
sinon.assert.calledWithExactly( spyHtmlProcessor, 'div' );
} );
} );
} );
1 change: 1 addition & 0 deletions packages/ckeditor5-html-embed/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"@ckeditor/ckeditor5-basic-styles": "^24.0.0",
"@ckeditor/ckeditor5-editor-classic": "^24.0.0",
"@ckeditor/ckeditor5-paragraph": "^24.0.0",
"@ckeditor/ckeditor5-clipboard": "^24.0.0",
"lodash-es": "^4.17.15",
"sanitize-html": "^2.1.0"
},
Expand Down
2 changes: 1 addition & 1 deletion packages/ckeditor5-html-embed/src/htmlembedediting.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export default class HtmlEmbedEditing extends Plugin {

// Register div.raw-html-embed as a raw content element so all of it's content will be provided
// as a view element's custom property while data upcasting.
editor.data.processor.registerRawContentMatcher( {
editor.data.registerRawContentMatcher( {
name: 'div',
classes: 'raw-html-embed'
} );
Expand Down
38 changes: 37 additions & 1 deletion packages/ckeditor5-html-embed/tests/htmlembedediting.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import UpdateHtmlEmbedCommand from '../src/updatehtmlembedcommand';
import InsertHtmlEmbedCommand from '../src/inserthtmlembedcommand';
import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
import { isWidget } from '@ckeditor/ckeditor5-widget/src/utils';
import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard';

describe( 'HtmlEmbedEditing', () => {
let element, editor, model, view, viewDocument;
Expand All @@ -24,7 +25,7 @@ describe( 'HtmlEmbedEditing', () => {

return ClassicTestEditor
.create( element, {
plugins: [ HtmlEmbedEditing ]
plugins: [ HtmlEmbedEditing, Clipboard ]
} )
.then( newEditor => {
editor = newEditor;
Expand Down Expand Up @@ -247,6 +248,33 @@ describe( 'HtmlEmbedEditing', () => {

expect( rawHtml.getAttribute( 'value' ) ).to.equal( rawContent );
} );

// See https://github.com/ckeditor/ckeditor5/issues/8789.
it( 'should convert content from clipboard', () => {
const dataTransferMock = createDataTransfer( {
'text/html':
'<div class="raw-html-embed">' +
'<b>Foo B.</b>' +
'<i>Foo I.</i>' +
'<u>Foo U.</u>' +
'</div>',
'text/plain': 'plain text'
} );

viewDocument.fire( 'paste', {
dataTransfer: dataTransferMock,
stopPropagation: sinon.spy(),
preventDefault: sinon.spy()
} );

const rawHtml = model.document.getRoot().getChild( 0 );

expect( rawHtml.getAttribute( 'value' ) ).to.equal(
'<b>Foo B.</b>' +
'<i>Foo I.</i>' +
'<u>Foo U.</u>'
);
} );
} );
} );

Expand Down Expand Up @@ -751,3 +779,11 @@ describe( 'HtmlEmbedEditing', () => {
function isRawHtmlWidget( viewElement ) {
return !!viewElement.getCustomProperty( 'rawHtml' ) && isWidget( viewElement );
}

function createDataTransfer( data ) {
return {
getData( type ) {
return data[ type ];
}
};
}

0 comments on commit 2025f40

Please sign in to comment.