Skip to content
This repository has been archived by the owner on Jun 26, 2020. It is now read-only.

T/404: Better handling of   block filler. #1779

Merged
merged 32 commits into from
Sep 18, 2019
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
c15a2b7
Better detection of block filler in data.
jodator Aug 6, 2019
63e81a5
Introduce isNegligibleBlockFiller helper function.
jodator Aug 7, 2019
fd2ae41
Update docs of isNegligibleBlockFiller() helper function.
jodator Aug 7, 2019
fd76bf2
Change the way how isBlockFiller check nbps fillers.
jodator Aug 8, 2019
be11659
The isBlockFiller() should allow single   inside inline elements.
jodator Aug 8, 2019
f9f89ae
Use more elements in `isInline()` check.
jodator Aug 8, 2019
0ac3b6b
Fix code style issues.
jodator Aug 8, 2019
2e9d550
Merge branch 'master' into t/404
jodator Aug 12, 2019
92da88e
wip
jodator Aug 12, 2019
2daf569
Revert to previous tests of nbsp between blocks.
jodator Aug 13, 2019
86a8970
Handle differently br and nbsp fillers.
jodator Aug 13, 2019
600dacb
Remove .only from tests.
jodator Aug 13, 2019
6053e18
Update "nbsp" between blocks tests cases.
jodator Aug 13, 2019
86eb560
Merge branch 'master' into t/404
jodator Aug 13, 2019
9ba5731
Simplify nbsp between blocks tests.
jodator Aug 14, 2019
37aec88
Merge branch 'master' into t/404
jodator Aug 26, 2019
c782dc9
Introduce blockFillerMode in DomConverter.
jodator Sep 3, 2019
d55bbde
Rewrite the isBlockFiller method with logic from DomConvrter#domToVie…
jodator Sep 3, 2019
e5c7361
Simplify isBlockFiller internals.
jodator Sep 3, 2019
1012605
Expand documentation for new block filler methods.
jodator Sep 4, 2019
dbecbf1
Remove unused import
jodator Sep 4, 2019
529a95d
Merge branch 'master' into t/404
jodator Sep 4, 2019
c62a9fd
Rename isNbspFillerFiller() to isNbspBlockFiller().
jodator Sep 4, 2019
75bad14
Filler docs fixes.
jodator Sep 4, 2019
326372c
Merge branch 'master' into t/404
jodator Sep 4, 2019
a9f4b7a
Docs fix.
jodator Sep 5, 2019
0a82f09
Merge branch 'master' into t/404
Reinmar Sep 17, 2019
d9dbda2
Update src/view/domconverter.js
jodator Sep 17, 2019
d370947
Update src/view/domconverter.js
jodator Sep 17, 2019
23b1b79
Move block filler handling to DomConverter.
jodator Sep 17, 2019
fc769ff
Merge branch 'master' into t/404
jodator Sep 17, 2019
fa8ccb6
Cleanup.
Reinmar Sep 18, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions src/dataprocessor/htmldataprocessor.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@

import BasicHtmlWriter from './basichtmlwriter';
import DomConverter from '../view/domconverter';
import { NBSP_FILLER } from '../view/filler';

/**
* The HTML data processor class.
Expand All @@ -38,7 +37,7 @@ export default class HtmlDataProcessor {
* @private
* @member {module:engine/view/domconverter~DomConverter}
*/
this._domConverter = new DomConverter( { blockFiller: NBSP_FILLER } );
this._domConverter = new DomConverter( { blockFillerMode: 'nbsp' } );

/**
* A basic HTML writer instance used to convert DOM elements to an HTML string.
Expand Down
3 changes: 1 addition & 2 deletions src/dataprocessor/xmldataprocessor.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@

import BasicHtmlWriter from './basichtmlwriter';
import DomConverter from '../view/domconverter';
import { NBSP_FILLER } from '../view/filler';

/**
* The XML data processor class.
Expand Down Expand Up @@ -54,7 +53,7 @@ export default class XmlDataProcessor {
* @private
* @member {module:engine/view/domconverter~DomConverter}
*/
this._domConverter = new DomConverter( { blockFiller: NBSP_FILLER } );
this._domConverter = new DomConverter( { blockFillerMode: 'nbsp' } );

/**
* A basic HTML writer instance used to convert DOM elements to an XML string.
Expand Down
37 changes: 27 additions & 10 deletions src/view/domconverter.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,15 @@ import ViewRange from './range';
import ViewSelection from './selection';
import ViewDocumentFragment from './documentfragment';
import ViewTreeWalker from './treewalker';
import { BR_FILLER, INLINE_FILLER_LENGTH, isBlockFiller, isInlineFiller, startsWithFiller, getDataWithoutFiller } from './filler';
import {
BR_FILLER,
NBSP_FILLER,
INLINE_FILLER_LENGTH,
isBlockFiller,
isInlineFiller,
startsWithFiller,
getDataWithoutFiller
} from './filler';

import global from '@ckeditor/ckeditor5-utils/src/dom/global';
import indexOf from '@ckeditor/ckeditor5-utils/src/dom/indexof';
Expand All @@ -42,7 +50,7 @@ export default class DomConverter {
* Creates DOM converter.
*
* @param {Object} options Object with configuration options.
* @param {Function} [options.blockFiller=module:engine/view/filler~BR_FILLER] Block filler creator.
* @param {module:engine/view/filler~blockFillerMode} [options.blockFillerMode='br'] Type of a block filler w
*/
constructor( options = {} ) {
// Using WeakMap prevent memory leaks: when the converter will be destroyed all referenced between View and DOM
Expand All @@ -56,13 +64,12 @@ export default class DomConverter {
// I've been here. Seen stuff. Afraid of code now.

/**
* Block {@link module:engine/view/filler filler} creator, which is used to create all block fillers during the
* view to DOM conversion and to recognize block fillers during the DOM to view conversion.
* The mode of a block filler used by DOM converter.
*
* @readonly
* @member {Function} module:engine/view/domconverter~DomConverter#blockFiller
* @member {String} module:engine/view/domconverter~DomConverter#blockFillerMode
jodator marked this conversation as resolved.
Show resolved Hide resolved
*/
this.blockFiller = options.blockFiller || BR_FILLER;
this.blockFillerMode = options.blockFillerMode || 'br';

/**
* Tag names of DOM `Element`s which are considered pre-formatted elements.
Expand All @@ -80,6 +87,16 @@ export default class DomConverter {
*/
this.blockElements = [ 'p', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' ];

/**
* Block {@link module:engine/view/filler filler} creator, which is used to create all block fillers during the
* view to DOM conversion and to recognize block fillers during the DOM to view conversion.
*
* @readonly
* @private
* @member {Function} module:engine/view/domconverter~DomConverter#_blockFiller
*/
this._blockFiller = this.blockFillerMode == 'br' ? BR_FILLER : NBSP_FILLER;

/**
* DOM to View mapping.
*
Expand Down Expand Up @@ -254,7 +271,7 @@ export default class DomConverter {

for ( const childView of viewElement.getChildren() ) {
if ( fillerPositionOffset === offset ) {
yield this.blockFiller( domDocument );
yield this._blockFiller( domDocument );
}

yield this.viewToDom( childView, domDocument, options );
Expand All @@ -263,7 +280,7 @@ export default class DomConverter {
}

if ( fillerPositionOffset === offset ) {
yield this.blockFiller( domDocument );
yield this._blockFiller( domDocument );
}
}

Expand Down Expand Up @@ -370,7 +387,7 @@ export default class DomConverter {
* or `null` if DOM node is a {@link module:engine/view/filler filler} or the given node is an empty text node.
*/
domToView( domNode, options = {} ) {
if ( isBlockFiller( domNode, this.blockFiller ) ) {
if ( isBlockFiller( domNode, this.blockFillerMode ) ) {
return null;
}

Expand Down Expand Up @@ -528,7 +545,7 @@ export default class DomConverter {
* @returns {module:engine/view/position~Position} viewPosition View position.
*/
domPositionToView( domParent, domOffset ) {
if ( isBlockFiller( domParent, this.blockFiller ) ) {
if ( isBlockFiller( domParent, this.blockFillerMode ) ) {
return this.domPositionToView( domParent.parentNode, indexOf( domParent ) );
}

Expand Down
62 changes: 47 additions & 15 deletions src/view/filler.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ export const BR_FILLER = domDocument => {
return fillerBr;
};

// eslint-disable-next-line new-cap
const BR_FILLER_REF = BR_FILLER( window.document );

/**
* Non-breaking space filler creator. This is a function which creates ` ` text node.
* It defines how the filler is created.
Expand Down Expand Up @@ -119,28 +122,21 @@ export function getDataWithoutFiller( domText ) {
}
}

// Cache block fillers templates to improve performance.
const templateBlockFillers = new WeakMap();

/**
* Checks if the node is an instance of the block filler of the given type.
*
* const brFillerInstance = BR_FILLER( document );
* isBlockFiller( brFillerInstance, BR_FILLER ); // true
* isBlockFiller( brFillerInstance, 'br' ); // true
* isBlockFiller( brFillerInstance, 'nbsp' ); // false
*
* **Note:**: For 'nbsp' mode the method also checks context of a node so it cannot be a detached node.
*
* @param {Node} domNode DOM node to check.
* @param {Function} blockFiller Block filler creator.
* @returns {Boolean} True if text node contains only {@link module:engine/view/filler~INLINE_FILLER inline filler}.
* @param {module:engine/view/filler~blockFillerMode} mode Mode of a block filler.
* @returns {Boolean} True if a node is considered a block filler for given mode.
*/
export function isBlockFiller( domNode, blockFiller ) {
let templateBlockFiller = templateBlockFillers.get( blockFiller );

if ( !templateBlockFiller ) {
templateBlockFiller = blockFiller( window.document );
templateBlockFillers.set( blockFiller, templateBlockFiller );
}

return domNode.isEqualNode( templateBlockFiller );
export function isBlockFiller( domNode, mode ) {
return mode == 'br' ? domNode.isEqualNode( BR_FILLER_REF ) : isNbspFillerFiller( domNode );
}

/**
Expand Down Expand Up @@ -168,3 +164,39 @@ function jumpOverInlineFiller( evt, data ) {
}
}
}

// Checks if given node is a nbsp block filler.
//
// A   is a block filler only if it is a single child of a block element.
//
// @param {Node} domNode DOM node.
// @returns {Boolean}
function isNbspFillerFiller( domNode ) {
const isNBSP = isText( domNode ) && domNode.data == '\u00A0';

return isNBSP && hasBlockParent( domNode ) && domNode.parentNode.childNodes.length === 1;
}

// Name of DOM nodes that are considered a block.
const blockElements = [ 'p', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' ];
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Reinmar I don't like that this duplicates the code from a DomConverter - maybe we should intorduce isBlockElement() in ckeditor5-utisl/dom? That this could be unified there.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use the same block elements array which is defined in DomConverter. The idea is that it can be extended by someone else if needed. So, we need to find a way to make filler.js us that property.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One option would be to have a module here (in the engine) which exports a table.

This module would be imported by both domconverter.js and filler.js. The instance of this array would be shared by those modules. So, if you'd extend DomConverter.blockElementNames that'd also extend the value used by the filler.js module.

The downside of this would be that this would become a singleton. All editor instances would share this table. In fact, that'd mean that if you'd like to extend this array in your plugin's code (cause it adds some new element support to the editor), if you'd run 10 editors on one page, this element would be added 10 times. That's quite ugly.

So, I'd rather go in a different direction – e.g. move isBlockFiller() to DomConverter. Perhaps it doesn't make sense to have this as contextless util. Maybe it needs to be a part of bigger context.


// Checks if domNode has block parent.
//
// @param {Node} domNode DOM node.
// @returns {Boolean}
function hasBlockParent( domNode ) {
const parent = domNode.parentNode;

return parent && parent.tagName && blockElements.includes( parent.tagName.toLowerCase() );
}

/**
* Enum representing type of the block filler.
*
* Possible values:
*
* * `br` - for `<br>` block filler used in editing view,
* * `nbsp` - for `&nbsp;` block fillers used in the data.
*
* @typedef {String} module:engine/view/filler~blockFillerMode
*/
10 changes: 5 additions & 5 deletions src/view/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -583,7 +583,7 @@ export default class Renderer {
_diffNodeLists( actualDomChildren, expectedDomChildren ) {
actualDomChildren = filterOutFakeSelectionContainer( actualDomChildren, this._fakeSelectionContainer );

return diff( actualDomChildren, expectedDomChildren, sameNodes.bind( null, this.domConverter.blockFiller ) );
return diff( actualDomChildren, expectedDomChildren, sameNodes.bind( null, this.domConverter.blockFillerMode ) );
}

/**
Expand Down Expand Up @@ -910,11 +910,11 @@ function areSimilar( node1, node2 ) {
// * Two block filler elements.
//
// @private
// @param {Function} blockFiller Block filler creator function, see {@link module:engine/view/domconverter~DomConverter#blockFiller}.
// @param {String} blockFillerMode Block filler mode, see {@link module:engine/view/domconverter~DomConverter#blockFillerMode}.
// @param {Node} node1
// @param {Node} node2
// @returns {Boolean}
function sameNodes( blockFiller, actualDomChild, expectedDomChild ) {
function sameNodes( blockFillerMode, actualDomChild, expectedDomChild ) {
// Elements.
if ( actualDomChild === expectedDomChild ) {
return true;
Expand All @@ -924,8 +924,8 @@ function sameNodes( blockFiller, actualDomChild, expectedDomChild ) {
return actualDomChild.data === expectedDomChild.data;
}
// Block fillers.
else if ( isBlockFiller( actualDomChild, blockFiller ) &&
isBlockFiller( expectedDomChild, blockFiller ) ) {
else if ( isBlockFiller( actualDomChild, blockFillerMode ) &&
isBlockFiller( expectedDomChild, blockFillerMode ) ) {
return true;
}

Expand Down
53 changes: 26 additions & 27 deletions tests/dataprocessor/htmldataprocessor.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,33 +59,32 @@ describe( 'HtmlDataProcessor', () => {
} );
}

// Uncomment this test after fixing #404.
// describe( 'https://github.com/ckeditor/ckeditor5-clipboard/issues/2#issuecomment-310417731 + #404', () => {
// it( 'does not lose whitespaces in Chrome\'s paste-like content', () => {
// const fragment = dataProcessor.toView(
// '<meta charset=\'utf-8\'>' +
// '<span>This is the<span>\u00a0</span></span>' +
// '<a href="url">third developer preview</a>' +
// '<span><span>\u00a0</span>of<span>\u00a0</span></span>' +
// '<strong>CKEditor\u00a05</strong>' +
// '<span>.</span>'
// );

// expect( stringify( fragment ) ).to.equal(
// '<span>This is the<span>\u00a0</span></span>' +
// '<a href="url">third developer preview</a>' +
// '<span><span>\u00a0</span>of<span>\u00a0</span></span>' +
// '<strong>CKEditor\u00a05</strong>' +
// '<span>.</span>'
// );

// // Just to be sure... stringify() uses conversion and the browser extensively,
// // so it's not entirely safe.
// expect( fragment.getChild( 0 ).getChild( 1 ).getChild( 0 ).data ).to.equal( '\u00a0' );
// expect( fragment.getChild( 2 ).getChild( 0 ).getChild( 0 ).data ).to.equal( '\u00a0' );
// expect( fragment.getChild( 2 ).getChild( 2 ).getChild( 0 ).data ).to.equal( '\u00a0' );
// } );
// } );
describe( 'https://github.com/ckeditor/ckeditor5-clipboard/issues/2#issuecomment-310417731 + #404', () => {
it( 'does not lose whitespaces in Chrome\'s paste-like content', () => {
const fragment = dataProcessor.toView(
'<meta charset=\'utf-8\'>' +
'<span>This is the<span>\u00a0</span></span>' +
'<a href="url">third developer preview</a>' +
'<span><span>\u00a0</span>of<span>\u00a0</span></span>' +
'<strong>CKEditor\u00a05</strong>' +
'<span>.</span>'
);

expect( stringify( fragment ) ).to.equal(
'<span>This is the<span>\u00a0</span></span>' +
'<a href="url">third developer preview</a>' +
'<span><span>\u00a0</span>of<span>\u00a0</span></span>' +
'<strong>CKEditor\u00a05</strong>' +
'<span>.</span>'
);

// Just to be sure... stringify() uses conversion and the browser extensively,
// so it's not entirely safe.
expect( fragment.getChild( 0 ).getChild( 1 ).getChild( 0 ).data ).to.equal( '\u00a0' );
expect( fragment.getChild( 2 ).getChild( 0 ).getChild( 0 ).data ).to.equal( '\u00a0' );
expect( fragment.getChild( 2 ).getChild( 2 ).getChild( 0 ).data ).to.equal( '\u00a0' );
} );
} );
} );

describe( 'toData()', () => {
Expand Down
10 changes: 6 additions & 4 deletions tests/view/domconverter/dom-to-view.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import ViewElement from '../../../src/view/element';
import ViewDocumentSelection from '../../../src/view/documentselection';
import DomConverter from '../../../src/view/domconverter';
import ViewDocumentFragment from '../../../src/view/documentfragment';
import { INLINE_FILLER, INLINE_FILLER_LENGTH, NBSP_FILLER } from '../../../src/view/filler';
import { INLINE_FILLER, INLINE_FILLER_LENGTH, NBSP_FILLER, BR_FILLER } from '../../../src/view/filler';

import { parse, stringify } from '../../../src/dev-utils/view';

Expand Down Expand Up @@ -157,7 +157,8 @@ describe( 'DomConverter', () => {
} );

it( 'should return null for block filler', () => {
const domFiller = converter.blockFiller( document );
// eslint-disable-next-line new-cap
const domFiller = BR_FILLER( document );

expect( converter.domToView( domFiller ) ).to.be.null;
} );
Expand Down Expand Up @@ -682,7 +683,8 @@ describe( 'DomConverter', () => {
} );

it( 'should skip filler', () => {
const domFiller = converter.blockFiller( document );
// eslint-disable-next-line new-cap
const domFiller = BR_FILLER( document );
const domP = createElement( document, 'p', null, domFiller );

const viewChildren = Array.from( converter.domChildrenToView( domP ) );
Expand Down Expand Up @@ -761,7 +763,7 @@ describe( 'DomConverter', () => {
} );

it( 'should converter position inside block filler', () => {
const converter = new DomConverter( { blockFiller: NBSP_FILLER } );
const converter = new DomConverter( { blockFillerMode: 'nbsp' } );
const domFiller = NBSP_FILLER( document ); // eslint-disable-line new-cap
const domP = createElement( document, 'p', null, domFiller );

Expand Down
12 changes: 6 additions & 6 deletions tests/view/domconverter/domconverter.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import ViewEditable from '../../../src/view/editableelement';
import ViewDocument from '../../../src/view/document';
import ViewUIElement from '../../../src/view/uielement';
import ViewContainerElement from '../../../src/view/containerelement';
import { BR_FILLER, NBSP_FILLER, INLINE_FILLER, INLINE_FILLER_LENGTH } from '../../../src/view/filler';
import { INLINE_FILLER, INLINE_FILLER_LENGTH } from '../../../src/view/filler';
import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
import global from '@ckeditor/ckeditor5-utils/src/dom/global';

Expand All @@ -24,13 +24,13 @@ describe( 'DomConverter', () => {
} );

describe( 'constructor()', () => {
it( 'should create converter with BR block filler by default', () => {
expect( converter.blockFiller ).to.equal( BR_FILLER );
it( 'should create converter with BR block filler mode by default', () => {
expect( converter.blockFillerMode ).to.equal( 'br' );
} );

it( 'should create converter with defined block filler', () => {
converter = new DomConverter( { blockFiller: NBSP_FILLER } );
expect( converter.blockFiller ).to.equal( NBSP_FILLER );
it( 'should create converter with defined block mode filler', () => {
converter = new DomConverter( { blockFillerMode: 'nbsp' } );
expect( converter.blockFillerMode ).to.equal( 'nbsp' );
} );
} );

Expand Down
4 changes: 2 additions & 2 deletions tests/view/domconverter/view-to-dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -634,7 +634,7 @@ describe( 'DomConverter', () => {
const domChildren = Array.from( converter.viewChildrenToDom( viewP, document ) );

expect( domChildren.length ).to.equal( 1 );
expect( isBlockFiller( domChildren[ 0 ], converter.blockFiller ) ).to.be.true;
expect( isBlockFiller( domChildren[ 0 ], converter.blockFillerMode ) ).to.be.true;
} );

it( 'should add filler according to fillerPositionOffset', () => {
Expand All @@ -644,7 +644,7 @@ describe( 'DomConverter', () => {
const domChildren = Array.from( converter.viewChildrenToDom( viewP, document ) );

expect( domChildren.length ).to.equal( 2 );
expect( isBlockFiller( domChildren[ 0 ], converter.blockFiller ) ).to.be.true;
expect( isBlockFiller( domChildren[ 0 ], converter.blockFillerMode ) ).to.be.true;
expect( domChildren[ 1 ].data ).to.equal( 'foo' );
} );

Expand Down
Loading