Procházet zdrojové kódy

Merge pull request #175 from ckeditor/t/72

Feature: Link will be now highlighted when selection is inside it. Closes #72.
Szymon Cofalik před 7 roky
rodič
revize
8064cee575

+ 81 - 1
packages/ckeditor5-link/src/linkediting.js

@@ -8,12 +8,20 @@
  */
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
-import { downcastAttributeToElement } from '@ckeditor/ckeditor5-engine/src/conversion/downcast-converters';
+import {
+	downcastAttributeToElement,
+	downcastMarkerToHighlight,
+	createViewElementFromHighlightDescriptor
+} from '@ckeditor/ckeditor5-engine/src/conversion/downcast-converters';
 import { upcastElementToAttribute } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
 import LinkCommand from './linkcommand';
 import UnlinkCommand from './unlinkcommand';
 import { createLinkElement } from './utils';
 import bindTwoStepCaretToAttribute from '@ckeditor/ckeditor5-engine/src/utils/bindtwostepcarettoattribute';
+import findLinkRange from './findlinkrange';
+import '../theme/link.css';
+import DocumentSelection from '@ckeditor/ckeditor5-engine/src/model/documentselection';
+import ModelSelection from '@ckeditor/ckeditor5-engine/src/model/selection';
 
 /**
  * The link engine feature.
@@ -55,5 +63,77 @@ export default class LinkEditing extends Plugin {
 
 		// Enable two-step caret movement for `linkHref` attribute.
 		bindTwoStepCaretToAttribute( editor.editing.view, editor.model, this, 'linkHref' );
+
+		// Setup highlight over selected link.
+		this._setupLinkHighlight();
+	}
+
+	/**
+	 * Adds highlight over link which has selection inside, together with two-step caret movement indicates whenever
+	 * user is typing inside the link.
+	 *
+	 * @private
+	 */
+	_setupLinkHighlight() {
+		const editor = this.editor;
+		const model = this.editor.model;
+		const doc = model.document;
+		const highlightDescriptor = {
+			id: 'linkBoundaries',
+			class: 'ck-link_selected',
+			priority: 1
+		};
+
+		// Convert linkBoundaries marker to view highlight.
+		editor.conversion.for( 'editingDowncast' )
+			.add( downcastMarkerToHighlight( {
+				model: 'linkBoundaries',
+				view: highlightDescriptor
+			} ) );
+
+		// Create marker over whole link when selection has "linkHref" attribute.
+		doc.registerPostFixer( writer => {
+			const selection = doc.selection;
+			const marker = model.markers.get( 'linkBoundaries' );
+
+			// Create marker over link when selection is inside or remove marker otherwise.
+			if ( selection.hasAttribute( 'linkHref' ) ) {
+				const modelRange = findLinkRange( selection.getFirstPosition(), selection.getAttribute( 'linkHref' ) );
+
+				if ( !marker || !marker.getRange().isEqual( modelRange ) ) {
+					writer.setMarker( 'linkBoundaries', modelRange );
+					return true;
+				}
+			} else if ( marker ) {
+				writer.removeMarker( 'linkBoundaries' );
+				return true;
+			}
+
+			return false;
+		} );
+
+		// Custom converter for selection's "linkHref" attribute - when collapsed selection has this attribute it is
+		// wrapped with <span> similar to that used by highlighting mechanism. This <span> will be merged together with
+		// highlight wrapper. This prevents link splitting When selection is at the beginning or at the end of the link.
+		// Without this converter:
+		//
+		//		<a href="url">{}</a><span class="ck-link_selected"><a href="url">foo</a></span>
+		//
+		// When converter is applied:
+		//
+		//		<span class="ck-link_selected"><a href="url">{}foo</a></span>
+		editor.editing.downcastDispatcher.on( 'attribute:linkHref', ( evt, data, conversionApi ) => {
+			const selection = data.item;
+
+			if ( !( selection instanceof DocumentSelection || selection instanceof ModelSelection ) || !selection.isCollapsed ) {
+				return;
+			}
+
+			const writer = conversionApi.writer;
+			const viewSelection = writer.document.selection;
+			const wrapper = createViewElementFromHighlightDescriptor( highlightDescriptor );
+
+			conversionApi.writer.wrap( viewSelection.getFirstRange(), wrapper );
+		} );
 	}
 }

+ 122 - 2
packages/ckeditor5-link/tests/linkediting.js

@@ -9,6 +9,7 @@ import UnlinkCommand from '../src/unlinkcommand';
 
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import Enter from '@ckeditor/ckeditor5-enter/src/enter';
 import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
 import { isLinkElement } from '../src/utils';
@@ -19,16 +20,17 @@ import { downcastAttributeToElement } from '@ckeditor/ckeditor5-engine/src/conve
 /* global document */
 
 describe( 'LinkEditing', () => {
-	let editor, model;
+	let editor, model, view;
 
 	beforeEach( () => {
 		return VirtualTestEditor
 			.create( {
-				plugins: [ Paragraph, LinkEditing ]
+				plugins: [ Paragraph, LinkEditing, Enter ]
 			} )
 			.then( newEditor => {
 				editor = newEditor;
 				model = editor.model;
+				view = editor.editing.view;
 			} );
 	} );
 
@@ -143,4 +145,122 @@ describe( 'LinkEditing', () => {
 			expect( editor.getData() ).to.equal( '<p><a href="url">a<f>b</f>c</a></p>' );
 		} );
 	} );
+
+	describe( 'highlight link boundaries', () => {
+		it( 'should create marker in model when selection is inside a link', () => {
+			expect( model.markers.has( 'linkBoundaries' ) ).to.be.false;
+
+			setModelData( model,
+				'<paragraph>foo <$text linkHref="url">b{}ar</$text> baz</paragraph>'
+			);
+
+			expect( model.markers.has( 'linkBoundaries' ) ).to.be.true;
+			const marker = model.markers.get( 'linkBoundaries' );
+			expect( marker.getStart().path ).to.deep.equal( [ 0, 4 ] );
+			expect( marker.getEnd().path ).to.deep.equal( [ 0, 7 ] );
+		} );
+
+		it( 'should convert link boundaries marker to proper view', () => {
+			setModelData( model,
+				'<paragraph>foo <$text linkHref="url">b{}ar</$text> baz</paragraph>'
+			);
+
+			expect( getViewData( view ) ).to.equal(
+				'<p>foo <span class="ck-link_selected"><a href="url">b{}ar</a></span> baz</p>'
+			);
+		} );
+
+		it( 'should work whenever selection has linkHref attribute - link start', () => {
+			setModelData( model,
+				'<paragraph>foo {}<$text linkHref="url">bar</$text> baz</paragraph>'
+			);
+
+			expect( model.document.selection.hasAttribute( 'linkHref' ) ).to.be.false;
+
+			model.change( writer => {
+				writer.setSelectionAttribute( 'linkHref', 'url' );
+			} );
+
+			expect( model.document.selection.hasAttribute( 'linkHref' ) ).to.be.true;
+			expect( model.markers.has( 'linkBoundaries' ) ).to.be.true;
+			const marker = model.markers.get( 'linkBoundaries' );
+			expect( marker.getStart().path ).to.deep.equal( [ 0, 4 ] );
+			expect( marker.getEnd().path ).to.deep.equal( [ 0, 7 ] );
+
+			expect( getViewData( view ) ).to.equal(
+				'<p>foo <span class="ck-link_selected"><a href="url">{}bar</a></span> baz</p>'
+			);
+		} );
+
+		it( 'should work whenever selection has linkHref attribute - link end', () => {
+			setModelData( model,
+				'<paragraph>foo <$text linkHref="url">bar</$text>{} baz</paragraph>'
+			);
+
+			expect( model.document.selection.hasAttribute( 'linkHref' ) ).to.be.true;
+			expect( model.markers.has( 'linkBoundaries' ) ).to.be.true;
+			const marker = model.markers.get( 'linkBoundaries' );
+			expect( marker.getStart().path ).to.deep.equal( [ 0, 4 ] );
+			expect( marker.getEnd().path ).to.deep.equal( [ 0, 7 ] );
+
+			expect( getViewData( view ) ).to.equal(
+				'<p>foo <span class="ck-link_selected"><a href="url">bar{}</a></span> baz</p>'
+			);
+		} );
+
+		it( 'should render highlight correctly after splitting the link', () => {
+			setModelData( model,
+				'<paragraph>foo <$text linkHref="url">li{}nk</$text> baz</paragraph>'
+			);
+
+			editor.execute( 'enter' );
+
+			expect( getModelData( model ) ).to.equal(
+				'<paragraph>foo <$text linkHref="url">li</$text></paragraph>' +
+				'<paragraph><$text linkHref="url">[]nk</$text> baz</paragraph>'
+			);
+
+			expect( model.document.selection.hasAttribute( 'linkHref' ) ).to.be.true;
+			expect( model.markers.has( 'linkBoundaries' ) ).to.be.true;
+			const marker = model.markers.get( 'linkBoundaries' );
+			expect( marker.getStart().path ).to.deep.equal( [ 1, 0 ] );
+			expect( marker.getEnd().path ).to.deep.equal( [ 1, 2 ] );
+		} );
+
+		it( 'should remove marker when selection is moved out from the link', () => {
+			setModelData( model,
+				'<paragraph>foo <$text linkHref="url">li{}nk</$text> baz</paragraph>'
+			);
+
+			expect( getViewData( view ) ).to.equal(
+				'<p>foo <span class="ck-link_selected"><a href="url">li{}nk</a></span> baz</p>'
+			);
+
+			expect( model.markers.has( 'linkBoundaries' ) ).to.be.true;
+			model.change( writer => writer.setSelection( model.document.getRoot().getChild( 0 ), 0 ) );
+
+			expect( model.markers.has( 'linkBoundaries' ) ).to.be.false;
+			expect( getViewData( view ) ).to.equal(
+				'<p>{}foo <a href="url">link</a> baz</p>'
+			);
+		} );
+
+		it( 'should work correctly when selection is moved inside link', () => {
+			setModelData( model,
+				'<paragraph>foo <$text linkHref="url">li{}nk</$text> baz</paragraph>'
+			);
+
+			expect( getViewData( view ) ).to.equal(
+				'<p>foo <span class="ck-link_selected"><a href="url">li{}nk</a></span> baz</p>'
+			);
+
+			expect( model.markers.has( 'linkBoundaries' ) ).to.be.true;
+			model.change( writer => writer.setSelection( model.document.getRoot().getChild( 0 ), 5 ) );
+
+			expect( model.markers.has( 'linkBoundaries' ) ).to.be.true;
+			expect( getViewData( view ) ).to.equal(
+				'<p>foo <span class="ck-link_selected"><a href="url">l{}ink</a></span> baz</p>'
+			);
+		} );
+	} );
 } );

+ 11 - 3
packages/ckeditor5-link/tests/linkui.js

@@ -9,6 +9,7 @@ import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictest
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
 import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
 
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import LinkEditing from '../src/linkediting';
@@ -248,8 +249,13 @@ describe( 'LinkUI', () => {
 				linkUIFeature._showUI();
 				const spy = testUtils.sinon.stub( balloon, 'updatePosition' ).returns( {} );
 
+				expect( getViewData( view ) ).to.equal(
+					'<p><span class="ck-link_selected"><a href="url">f{}oo</a></span></p>'
+				);
+
 				const root = viewDocument.getRoot();
-				const text = root.getChild( 0 ).getChild( 0 ).getChild( 0 );
+				const linkElement = root.getChild( 0 ).getChild( 0 ).getChild( 0 );
+				const text = linkElement.getChild( 0 );
 
 				// Move selection to foo[].
 				view.change( writer => {
@@ -258,7 +264,7 @@ describe( 'LinkUI', () => {
 
 				sinon.assert.calledOnce( spy );
 				sinon.assert.calledWithExactly( spy, {
-					target: view.domConverter.mapViewToDom( root.getChild( 0 ).getChild( 0 ) )
+					target: view.domConverter.mapViewToDom( linkElement )
 				} );
 			} );
 
@@ -313,8 +319,10 @@ describe( 'LinkUI', () => {
 				const spyUpdate = testUtils.sinon.stub( balloon, 'updatePosition' ).returns( {} );
 				const spyHide = testUtils.sinon.spy( linkUIFeature, '_hideUI' );
 
+				expect( getViewData( view ) ).to.equal( '<p><span class="ck-link_selected"><a href="url">f{}oo</a></span></p>' );
+
 				const root = viewDocument.getRoot();
-				const text = root.getChild( 0 ).getChild( 0 ).getChild( 0 );
+				const text = root.getChild( 0 ).getChild( 0 ).getChild( 0 ).getChild( 0 );
 
 				// Move selection to f[o]o.
 				view.change( writer => {

+ 3 - 0
packages/ckeditor5-link/tests/manual/linkhighlight.html

@@ -0,0 +1,3 @@
+<div id="editor">
+	<p>foo <a href="http://ckeditor.com">linked text</a> baz</p>
+</div>

+ 25 - 0
packages/ckeditor5-link/tests/manual/linkhighlight.js

@@ -0,0 +1,25 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals console:false, window, document */
+
+import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
+import Enter from '@ckeditor/ckeditor5-enter/src/enter';
+import Typing from '@ckeditor/ckeditor5-typing/src/typing';
+import Link from '../../src/link';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import Undo from '@ckeditor/ckeditor5-undo/src/undo';
+
+ClassicEditor
+	.create( document.querySelector( '#editor' ), {
+		plugins: [ Link, Typing, Paragraph, Undo, Enter ],
+		toolbar: [ 'link', 'undo', 'redo' ]
+	} )
+	.then( editor => {
+		window.editor = editor;
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );

+ 8 - 0
packages/ckeditor5-link/tests/manual/linkhighlight.md

@@ -0,0 +1,8 @@
+## Link highlight
+
+1. Put selection inside "linked text" - whole linked text should be highlighted.
+1. Move selection inside linked text, it should stay highlighted.
+1. Check if it works correctly with two-step caret movement when entering and leaving linked text.
+1. Use enter to split linked text, check if links are highlighted separately.
+
+NOTE: No additional undo steps should be added when preparing steps 1-3.

+ 10 - 0
packages/ckeditor5-link/theme/link.css

@@ -0,0 +1,10 @@
+/*
+ * Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/*
+ * Note: This file should contain the wireframe styles only. But since there are no such styles,
+ * it acts as a message to the builder telling that it should look for the corresponding styles
+ * **in the theme** when compiling the editor.
+ */