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

Commit

Permalink
Merge fc769ff into f073ad5
Browse files Browse the repository at this point in the history
  • Loading branch information
jodator authored Sep 17, 2019
2 parents f073ad5 + fc769ff commit 09b2cac
Show file tree
Hide file tree
Showing 13 changed files with 284 additions and 147 deletions.
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
84 changes: 73 additions & 11 deletions src/view/domconverter.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* @module engine/view/domconverter
*/

/* globals document, Node, NodeFilter, Text */
/* globals window, document, Node, NodeFilter, Text */

import ViewText from './text';
import ViewElement from './element';
Expand All @@ -16,7 +16,7 @@ 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, getDataWithoutFiller, INLINE_FILLER_LENGTH, isInlineFiller, NBSP_FILLER, startsWithFiller } from './filler';

import global from '@ckeditor/ckeditor5-utils/src/dom/global';
import indexOf from '@ckeditor/ckeditor5-utils/src/dom/indexof';
Expand All @@ -25,6 +25,9 @@ import getCommonAncestor from '@ckeditor/ckeditor5-utils/src/dom/getcommonancest
import isText from '@ckeditor/ckeditor5-utils/src/dom/istext';
import { isElement } from 'lodash-es';

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

/**
* DomConverter is a set of tools to do transformations between DOM nodes and view nodes. It also handles
* {@link module:engine/view/domconverter~DomConverter#bindElements binding} these nodes.
Expand All @@ -42,7 +45,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'] The type of the block filler to use.
*/
constructor( options = {} ) {
// Using WeakMap prevent memory leaks: when the converter will be destroyed all referenced between View and DOM
Expand All @@ -56,13 +59,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 {'br'|'nbsp'} module:engine/view/domconverter~DomConverter#blockFillerMode
*/
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 +82,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 @@ -255,7 +267,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 @@ -264,7 +276,7 @@ export default class DomConverter {
}

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

Expand Down Expand Up @@ -371,7 +383,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 ( this.isBlockFiller( domNode, this.blockFillerMode ) ) {
return null;
}

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

Expand Down Expand Up @@ -786,6 +798,23 @@ export default class DomConverter {
return node && node.nodeType == Node.COMMENT_NODE;
}

/**
* Checks if the node is an instance of the block filler for this DOM converter.
*
* const converter = new DomConverter( { blockFillerMode: 'br' } );
*
* converter.isBlockFiller( BR_FILLER( document ) ); // true
* converter.isBlockFiller( NBSP_FILLER( document ) ); // false
*
* **Note:**: For the `'nbsp'` mode the method also checks context of a node so it cannot be a detached node.
*
* @param {Node} domNode DOM node to check.
* @returns {Boolean} True if a node is considered a block filler for given mode.
*/
isBlockFiller( domNode ) {
return this.blockFillerMode == 'br' ? domNode.isEqualNode( BR_FILLER_REF ) : isNbspBlockFiller( domNode, this.blockElements );
}

/**
* Returns `true` if given selection is a backward selection, that is, if it's `focus` is before `anchor`.
*
Expand Down Expand Up @@ -1197,3 +1226,36 @@ function forEachDomNodeAncestor( node, callback ) {
node = node.parentNode;
}
}

// 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 isNbspBlockFiller( domNode, blockElements ) {
const isNBSP = isText( domNode ) && domNode.data == '\u00A0';

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

// Checks if domNode has block parent.
//
// @param {Node} domNode DOM node.
// @returns {Boolean}
function hasBlockParent( domNode, blockElements ) {
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
*/
58 changes: 18 additions & 40 deletions src/view/filler.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/

/* globals window */

import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
import isText from '@ckeditor/ckeditor5-utils/src/dom/istext';

Expand Down Expand Up @@ -36,6 +34,15 @@ import isText from '@ckeditor/ckeditor5-utils/src/dom/istext';
* @module engine/view/filler
*/

/**
* Non-breaking space filler creator. This is a function which creates `&nbsp;` text node.
* It defines how the filler is created.
*
* @see module:engine/view/filler~BR_FILLER
* @function
*/
export const NBSP_FILLER = domDocument => domDocument.createTextNode( '\u00A0' );

/**
* `<br>` filler creator. This is a function which creates `<br data-cke-filler="true">` element.
* It defines how the filler is created.
Expand All @@ -50,28 +57,23 @@ export const BR_FILLER = domDocument => {
return fillerBr;
};

/**
* Non-breaking space filler creator. This is a function which creates `&nbsp;` text node.
* It defines how the filler is created.
*
* @see module:engine/view/filler~BR_FILLER
* @function
*/
export const NBSP_FILLER = domDocument => domDocument.createTextNode( '\u00A0' );

/**
* Length of the {@link module:engine/view/filler~INLINE_FILLER INLINE_FILLER}.
*/
export const INLINE_FILLER_LENGTH = 7;

/**
* Inline filler which is sequence of the zero width spaces.
* Inline filler which is a sequence of the zero width spaces.
*/
export let INLINE_FILLER = '';
export const INLINE_FILLER = ( () => {
let inlineFiller = '';

for ( let i = 0; i < INLINE_FILLER_LENGTH; i++ ) {
INLINE_FILLER += '\u200b';
}
for ( let i = 0; i < INLINE_FILLER_LENGTH; i++ ) {
inlineFiller += '\u200b';
}

return inlineFiller;
} )(); // Usu IIF so the INLINE_FILLER appears as a constant in the docs.

/**
* Checks if the node is a text node which starts with the {@link module:engine/view/filler~INLINE_FILLER inline filler}.
Expand Down Expand Up @@ -119,30 +121,6 @@ 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
*
* @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}.
*/
export function isBlockFiller( domNode, blockFiller ) {
let templateBlockFiller = templateBlockFillers.get( blockFiller );

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

return domNode.isEqualNode( templateBlockFiller );
}

/**
* Assign key observer which move cursor from the end of the inline filler to the beginning of it when
* the left arrow is pressed, so the filler does not break navigation.
Expand Down
12 changes: 6 additions & 6 deletions src/view/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

import ViewText from './text';
import ViewPosition from './position';
import { INLINE_FILLER, INLINE_FILLER_LENGTH, startsWithFiller, isInlineFiller, isBlockFiller } from './filler';
import { INLINE_FILLER, INLINE_FILLER_LENGTH, startsWithFiller, isInlineFiller } from './filler';

import mix from '@ckeditor/ckeditor5-utils/src/mix';
import diff from '@ckeditor/ckeditor5-utils/src/diff';
Expand Down Expand Up @@ -590,7 +590,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 ) );
}

/**
Expand Down Expand Up @@ -938,11 +938,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( domConverter, actualDomChild, expectedDomChild ) {
// Elements.
if ( actualDomChild === expectedDomChild ) {
return true;
Expand All @@ -952,8 +952,8 @@ function sameNodes( blockFiller, actualDomChild, expectedDomChild ) {
return actualDomChild.data === expectedDomChild.data;
}
// Block fillers.
else if ( isBlockFiller( actualDomChild, blockFiller ) &&
isBlockFiller( expectedDomChild, blockFiller ) ) {
else if ( domConverter.isBlockFiller( actualDomChild ) &&
domConverter.isBlockFiller( expectedDomChild ) ) {
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
Loading

0 comments on commit 09b2cac

Please sign in to comment.