Przeglądaj źródła

Introduced view.writer.clear function.

Oskar Wróbel 9 lat temu
rodzic
commit
0ec7355100

+ 64 - 0
packages/ckeditor5-engine/src/view/writer.js

@@ -11,12 +11,15 @@ import Position from './position';
 import ContainerElement from './containerelement';
 import AttributeElement from './attributeelement';
 import EmptyElement from './emptyelement';
+import Element from './element';
 import Text from './text';
+import TextProxy from './textproxy';
 import Range from './range';
 import TreeWalker from './treewalker';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import DocumentFragment from './documentfragment';
 import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable';
+import { stringify, parse } from '../dev-utils/view';
 
 /**
  * Contains functions used for composing view tree.
@@ -398,6 +401,67 @@ export function remove( range ) {
 	return new DocumentFragment( removed );
 }
 
+/**
+ * Removes matches elements from given range.
+ *
+ * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when
+ * {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions are not placed inside
+ * same parent container.
+ *
+ * @function module:engine/view/writer~writer.clear
+ * @param {module:engine/view/range~Range} range Range to clear.
+ * @param {module:engine/view/element~Element} element Element to remove.
+ */
+export function clear( range, element ) {
+	validateRangeContainer( range );
+
+	// Create walker on given range.
+	// We are walking backward because when we remove element during walk it modify range end position.
+	const rangeWalker = range.getWalker( {
+		direction: 'backward',
+		ignoreElementEnd: true
+	} );
+
+	// Let's walk.
+	for ( const current of rangeWalker ) {
+		const item = current.item;
+		let rangeToRemove;
+
+		// When current item matches to the given element.
+		if ( item instanceof Element && element.isSimilar( item ) ) {
+			// Create range on this element.
+			rangeToRemove = Range.createOn( item );
+		// When range starts inside Text or TextProxy element.
+		} else if ( !current.nextPosition.isAfter( range.start ) && ( item instanceof Text || item instanceof TextProxy ) ) {
+			// We need to check if parent of this text matches to given element.
+			const parentElement = item.getAncestors().find( ( ancestor ) => {
+				return ancestor instanceof Element && element.isSimilar( ancestor );
+			} );
+
+			// If it is then create a range inside this element.
+			if ( parentElement && parentElement) {
+				rangeToRemove = Range.createIn( parentElement );
+			}
+		}
+
+		// If we have found element to remove.
+		if ( rangeToRemove ) {
+			// We need to check if element range stick out of given range and truncate if it is.
+			if ( !range.containsRange( rangeToRemove ) && range.isIntersecting( rangeToRemove ) ) {
+				if ( rangeToRemove.end.isAfter( range.end ) ) {
+					rangeToRemove.end = range.end;
+				}
+
+				if ( rangeToRemove.start.isBefore( range.start ) ) {
+					rangeToRemove.start = range.start;
+				}
+			}
+
+			remove( rangeToRemove );
+		}
+	}
+}
+
 /**
  * Moves nodes from provided range to target position.
  *

+ 148 - 0
packages/ckeditor5-engine/tests/view/writer/clear.js

@@ -0,0 +1,148 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import { clear } from '../../../src/view/writer';
+import Range from '../../../src/view/range';
+import { stringify, parse } from '../../../src/dev-utils/view';
+import ContainerElement from '../../../src/view/containerelement';
+import AttributeElement from '../../../src/view/attributeelement';
+import EmptyElement from '../../../src/view/emptyelement';
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+
+describe( 'writer', () => {
+	// Executes test using `parse` and `stringify` utils functions. Uses range delimiters `[]{}` to create ranges.
+	//
+	// @param {Object} elementToRemove
+	// @param {String} input
+	// @param {String} expectedResult
+	function test( elementToRemove, input, expectedResult ) {
+		let { view, selection } = parse( input );
+
+		clear( selection.getFirstRange(), elementToRemove );
+
+		expect( stringify( view, null, { showType: true } ) ).to.equal( expectedResult );
+	}
+
+	describe( 'clear', () => {
+		it( 'should throw when range placed in two containers', () => {
+			const p1 = new ContainerElement( 'p' );
+			const p2 = new ContainerElement( 'p' );
+
+			expect( () => {
+				clear( Range.createFromParentsAndOffsets( p1, 0, p2, 0 ) );
+			} ).to.throw( CKEditorError, 'view-writer-invalid-range-container' );
+		} );
+
+		it( 'should throw when range has no parent container', () => {
+			const el = new AttributeElement( 'b' );
+
+			expect( () => {
+				clear( Range.createFromParentsAndOffsets( el, 0, el, 0 ) );
+			} ).to.throw( CKEditorError, 'view-writer-invalid-range-container' );
+		} );
+
+		it( 'should remove matches element from range', () => {
+			const elementToRemove = new AttributeElement( 'b' );
+
+			test(
+				elementToRemove,
+				'<container:p>[b<attribute:b>a</attribute:b>r]</container:p>',
+				'<container:p>br</container:p>'
+			);
+		} );
+
+		it( 'should remove matches element from range when range is inside text node', () => {
+			const elementToRemove = new AttributeElement( 'b' );
+
+			test(
+				elementToRemove,
+				'<container:p>F{o<attribute:b>o</attribute:b> ba}r</container:p>',
+				'<container:p>Fo bar</container:p>'
+			);
+		} );
+
+		it( 'should remove matches elements from range', () => {
+			const elementToRemove = new AttributeElement( 'b' );
+
+			test(
+				elementToRemove,
+				'<container:p>[Fo<attribute:b>o</attribute:b> b<attribute:b>a</attribute:b>r]</container:p>',
+				'<container:p>Fo br</container:p>'
+			);
+		} );
+
+		it( 'should remove matches elements from range when range is inside text node', () => {
+			const elementToRemove = new AttributeElement( 'b' );
+
+			test(
+				elementToRemove,
+				'<container:p>F{o<attribute:b>o</attribute:b> <attribute:b>b</attribute:b>a}r</container:p>',
+				'<container:p>Fo ar</container:p>'
+			);
+		} );
+
+		it( 'should remove only matches element', () => {
+			const elementToRemove = new AttributeElement( 'b' );
+
+			test(
+				elementToRemove,
+				'<container:p>F{o<attribute:i>o</attribute:i> <attribute:b>b</attribute:b>a}r</container:p>',
+				'<container:p>Fo<attribute:i>o</attribute:i> ar</container:p>'
+			);
+		} );
+
+		it( 'should remove part of node when range ends inside this node', () => {
+			const elementToRemove = new AttributeElement( 'b' );
+
+			test(
+				elementToRemove,
+				'<container:p>f{oo<attribute:b>ba}r</attribute:b></container:p>',
+				'<container:p>foo<attribute:b>r</attribute:b></container:p>'
+			);
+		} );
+
+		it( 'should remove part of node when range starts inside this node', () => {
+			const elementToRemove = new AttributeElement( 'b' );
+
+			test(
+				elementToRemove,
+				'<container:p>foo<attribute:b>b{ar</attribute:b>bi}z</container:p>',
+				'<container:p>foo<attribute:b>b</attribute:b>biz</container:p>'
+			);
+		} );
+
+		it( 'should remove part of node when range starts and ends inside this node', () => {
+			const elementToRemove = new AttributeElement( 'b' );
+
+			test(
+				elementToRemove,
+				'<container:p>foo<attribute:b>b{a}r</attribute:b>biz</container:p>',
+				'<container:p>foo<attribute:b>br</attribute:b>biz</container:p>'
+			);
+		} );
+
+		it( 'should merge after removing', () => {
+			const elementToRemove = new AttributeElement( 'b' );
+
+			test(
+				elementToRemove,
+				'<container:p>' +
+					'<attribute:a>fo{o</attribute:a><attribute:b>a</attribute:b><attribute:a>b}iz</attribute:a>' +
+				'</container:p>',
+				'<container:p><attribute:a>foobiz</attribute:a></container:p>'
+			);
+		} );
+
+		it( 'should remove EmptyElement', () => {
+			const elementToRemove = new EmptyElement( 'img' );
+
+			test(
+				elementToRemove,
+				'<container:p>f{oo<empty:img></empty:img>ba}r</container:p>',
+				'<container:p>foobar</container:p>'
+			);
+		} );
+	} );
+} );

+ 1 - 1
packages/ckeditor5-engine/tests/view/writer/remove.js

@@ -70,7 +70,7 @@ describe( 'writer', () => {
 			test( '<container:p>f{oob}ar</container:p>', '<container:p>f{}ar</container:p>', 'oob' );
 		} );
 
-		it( 'should remove parts of nodes', () => {
+		it( 'should remove parts of nodes #1', () => {
 			test(
 				'<container:p>f{oo<attribute:b view-priority="10">ba}r</attribute:b></container:p>',
 				'<container:p>f[]<attribute:b view-priority="10">r</attribute:b></container:p>',