diff --git a/packages/ckeditor5-clipboard/src/clipboard.js b/packages/ckeditor5-clipboard/src/clipboard.js index 8e1bd943fe3..aaecbfbd0eb 100644 --- a/packages/ckeditor5-clipboard/src/clipboard.js +++ b/packages/ckeditor5-clipboard/src/clipboard.js @@ -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'; /** @@ -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. @@ -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, { @@ -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 ) ); } diff --git a/packages/ckeditor5-editor-balloon/src/ballooneditor.js b/packages/ckeditor5-editor-balloon/src/ballooneditor.js index cc30482c844..19f390e4ab2 100644 --- a/packages/ckeditor5-editor-balloon/src/ballooneditor.js +++ b/packages/ckeditor5-editor-balloon/src/ballooneditor.js @@ -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'; @@ -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 ); diff --git a/packages/ckeditor5-editor-classic/src/classiceditor.js b/packages/ckeditor5-editor-classic/src/classiceditor.js index 5c1c413b657..7242cddfae4 100644 --- a/packages/ckeditor5-editor-classic/src/classiceditor.js +++ b/packages/ckeditor5-editor-classic/src/classiceditor.js @@ -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'; @@ -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' ); diff --git a/packages/ckeditor5-editor-decoupled/src/decouplededitor.js b/packages/ckeditor5-editor-decoupled/src/decouplededitor.js index b79c07677cc..4fcf609f4aa 100644 --- a/packages/ckeditor5-editor-decoupled/src/decouplededitor.js +++ b/packages/ckeditor5-editor-decoupled/src/decouplededitor.js @@ -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'; @@ -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' ); diff --git a/packages/ckeditor5-editor-inline/src/inlineeditor.js b/packages/ckeditor5-editor-inline/src/inlineeditor.js index 81e7287ffe7..8c091b97f75 100644 --- a/packages/ckeditor5-editor-inline/src/inlineeditor.js +++ b/packages/ckeditor5-editor-inline/src/inlineeditor.js @@ -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'; @@ -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 ) ) { diff --git a/packages/ckeditor5-engine/src/controller/datacontroller.js b/packages/ckeditor5-engine/src/controller/datacontroller.js index 66bd7b557cb..2bada2c8b9d 100644 --- a/packages/ckeditor5-engine/src/controller/datacontroller.js +++ b/packages/ckeditor5-engine/src/controller/datacontroller.js @@ -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 @@ -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 @@ -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}. @@ -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. */ diff --git a/packages/ckeditor5-engine/tests/controller/datacontroller.js b/packages/ckeditor5-engine/tests/controller/datacontroller.js index 0317ac9c5e5..ae5374a7c54 100644 --- a/packages/ckeditor5-engine/tests/controller/datacontroller.js +++ b/packages/ckeditor5-engine/tests/controller/datacontroller.js @@ -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(); @@ -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 ] ); } ); @@ -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()', () => { @@ -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' ); + } ); + } ); } ); diff --git a/packages/ckeditor5-html-embed/package.json b/packages/ckeditor5-html-embed/package.json index efecf168890..f98f874d63c 100644 --- a/packages/ckeditor5-html-embed/package.json +++ b/packages/ckeditor5-html-embed/package.json @@ -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" }, diff --git a/packages/ckeditor5-html-embed/src/htmlembedediting.js b/packages/ckeditor5-html-embed/src/htmlembedediting.js index f70cd4f7828..117c973952d 100644 --- a/packages/ckeditor5-html-embed/src/htmlembedediting.js +++ b/packages/ckeditor5-html-embed/src/htmlembedediting.js @@ -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' } ); diff --git a/packages/ckeditor5-html-embed/tests/htmlembedediting.js b/packages/ckeditor5-html-embed/tests/htmlembedediting.js index 64b1c2f0f53..54cd05c5aae 100644 --- a/packages/ckeditor5-html-embed/tests/htmlembedediting.js +++ b/packages/ckeditor5-html-embed/tests/htmlembedediting.js @@ -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; @@ -24,7 +25,7 @@ describe( 'HtmlEmbedEditing', () => { return ClassicTestEditor .create( element, { - plugins: [ HtmlEmbedEditing ] + plugins: [ HtmlEmbedEditing, Clipboard ] } ) .then( newEditor => { editor = newEditor; @@ -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': + '
' + + 'Foo B.' + + 'Foo I.' + + 'Foo U.' + + '
', + '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( + 'Foo B.' + + 'Foo I.' + + 'Foo U.' + ); + } ); } ); } ); @@ -751,3 +779,11 @@ describe( 'HtmlEmbedEditing', () => { function isRawHtmlWidget( viewElement ) { return !!viewElement.getCustomProperty( 'rawHtml' ) && isWidget( viewElement ); } + +function createDataTransfer( data ) { + return { + getData( type ) { + return data[ type ]; + } + }; +}