/**
* @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
import LinkEditing from '../src/linkediting';
import LinkCommand from '../src/linkcommand';
import UnlinkCommand from '../src/unlinkcommand';
import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
import Enter from '@ckeditor/ckeditor5-enter/src/enter';
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
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';
import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
import Typing from '@ckeditor/ckeditor5-typing/src/typing';
import BoldEditing from '@ckeditor/ckeditor5-basic-styles/src/bold/boldediting';
/* global document */
describe( 'LinkEditing', () => {
let element, editor, model, view;
beforeEach( async () => {
element = document.createElement( 'div' );
document.body.appendChild( element );
editor = await ClassicTestEditor.create( element, {
plugins: [ Paragraph, LinkEditing, Enter ],
link: {
decorators: {
isExternal: {
mode: 'manual',
label: 'Open in a new window',
attributes: {
target: '_blank',
rel: 'noopener noreferrer'
}
}
}
}
} );
editor.model.schema.extend( '$text', { allowAttributes: 'bold' } );
editor.conversion.attributeToElement( {
model: 'bold',
view: 'b'
} );
model = editor.model;
view = editor.editing.view;
} );
afterEach( async () => {
element.remove();
await editor.destroy();
} );
it( 'should have pluginName', () => {
expect( LinkEditing.pluginName ).to.equal( 'LinkEditing' );
} );
it( 'should be loaded', () => {
expect( editor.plugins.get( LinkEditing ) ).to.be.instanceOf( LinkEditing );
} );
it( 'should set proper schema rules', () => {
expect( model.schema.checkAttribute( [ '$block', '$text' ], 'linkHref' ) ).to.be.true;
expect( model.schema.checkAttribute( [ '$clipboardHolder', '$text' ], 'linkHref' ) ).to.be.true;
expect( model.schema.checkAttribute( [ '$block' ], 'linkHref' ) ).to.be.false;
} );
// Let's check only the minimum to not duplicate `TwoStepCaretMovement` tests.
// Testing minimum is better than testing using spies that might give false positive results.
describe( 'two-step caret movement', () => {
it( 'should be bound to the `linkHref` attribute (LTR)', () => {
const selection = editor.model.document.selection;
// Put selection before the link element.
setModelData( editor.model, 'foo[]<$text linkHref="url">b$text>ar' );
// The selection's gravity should read attributes from the left.
expect( selection.hasAttribute( 'linkHref' ), 'hasAttribute( \'linkHref\' )' ).to.be.false;
// So let's simulate the `keydown` event.
editor.editing.view.document.fire( 'keydown', {
keyCode: keyCodes.arrowright,
preventDefault: () => {},
domTarget: document.body
} );
expect( getModelData( model ) ).to.equal( 'foo<$text linkHref="url">[]b$text>ar' );
// Selection should get the attributes from the right.
expect( selection.hasAttribute( 'linkHref' ), 'hasAttribute( \'linkHref\' )' ).to.be.true;
expect( selection.getAttribute( 'linkHref' ), 'linkHref attribute' ).to.equal( 'url' );
} );
it( 'should be bound to the `linkHref` attribute (RTL)', async () => {
const editor = await ClassicTestEditor.create( element, {
plugins: [ Paragraph, LinkEditing, Enter ],
language: {
content: 'ar'
}
} );
model = editor.model;
view = editor.editing.view;
const selection = editor.model.document.selection;
// Put selection before the link element.
setModelData( editor.model, 'foo[]<$text linkHref="url">b$text>ar' );
// The selection's gravity should read attributes from the left.
expect( selection.hasAttribute( 'linkHref' ), 'hasAttribute( \'linkHref\' )' ).to.be.false;
// So let's simulate the `keydown` event.
editor.editing.view.document.fire( 'keydown', {
keyCode: keyCodes.arrowleft,
preventDefault: () => {},
domTarget: document.body
} );
expect( getModelData( model ) ).to.equal( 'foo<$text linkHref="url">[]b$text>ar' );
// Selection should get the attributes from the right.
expect( selection.hasAttribute( 'linkHref' ), 'hasAttribute( \'linkHref\' )' ).to.be.true;
expect( selection.getAttribute( 'linkHref' ), 'linkHref attribute' ).to.equal( 'url' );
await editor.destroy();
} );
} );
// https://github.com/ckeditor/ckeditor5/issues/6053
describe( 'selection attribute management on paste', () => {
it( 'should remove link atttributes when pasting a link', () => {
setModelData( model, 'foo[]' );
model.change( writer => {
model.insertContent( writer.createText( 'INSERTED', { linkHref: 'ckeditor.com' } ) );
} );
expect( getModelData( model ) ).to.equal( 'foo<$text linkHref="ckeditor.com">INSERTED$text>[]' );
expect( [ ...model.document.selection.getAttributeKeys() ] ).to.be.empty;
} );
it( 'should remove all atttributes starting with "link" (e.g. decorator attributes) when pasting a link', () => {
setModelData( model, 'foo[]' );
model.change( writer => {
model.insertContent( writer.createText( 'INSERTED', { linkHref: 'ckeditor.com', linkIsExternal: true } ) );
} );
expect( getModelData( model ) ).to.equal(
'' +
'foo<$text linkHref="ckeditor.com" linkIsExternal="true">INSERTED$text>[]' +
''
);
expect( [ ...model.document.selection.getAttributeKeys() ] ).to.be.empty;
} );
it( 'should not remove link atttributes when pasting a non-link content', () => {
setModelData( model, '<$text linkHref="ckeditor.com">foo[]$text>' );
model.change( writer => {
model.insertContent( writer.createText( 'INSERTED', { bold: 'true' } ) );
} );
expect( getModelData( model ) ).to.equal(
'' +
'<$text linkHref="ckeditor.com">foo$text>' +
'<$text bold="true">INSERTED[]$text>' +
''
);
expect( [ ...model.document.selection.getAttributeKeys() ] ).to.have.members( [ 'bold' ] );
} );
it( 'should not remove link atttributes when pasting in the middle of a link with the same URL', () => {
setModelData( model, '<$text linkHref="ckeditor.com">fo[]o$text>' );
model.change( writer => {
model.insertContent( writer.createText( 'INSERTED', { linkHref: 'ckeditor.com' } ) );
} );
expect( getModelData( model ) ).to.equal( '<$text linkHref="ckeditor.com">foINSERTED[]o$text>' );
expect( [ ...model.document.selection.getAttributeKeys() ] ).to.have.members( [ 'linkHref' ] );
} );
it( 'should not remove link atttributes from the selection when pasting before a link when the gravity is overridden', () => {
setModelData( model, 'foo[]<$text linkHref="ckeditor.com">bar$text>' );
view.document.fire( 'keydown', {
keyCode: keyCodes.arrowright,
preventDefault: () => {},
domTarget: document.body
} );
expect( model.document.selection.isGravityOverridden ).to.be.true;
model.change( writer => {
model.insertContent( writer.createText( 'INSERTED', { bold: true } ) );
} );
expect( getModelData( model ) ).to.equal(
'' +
'foo' +
'<$text bold="true">INSERTED$text>' +
'<$text linkHref="ckeditor.com">[]bar$text>' +
''
);
expect( model.document.selection.isGravityOverridden ).to.be.true;
expect( [ ...model.document.selection.getAttributeKeys() ] ).to.have.members( [ 'linkHref' ] );
} );
it( 'should not remove link atttributes when pasting a link into another link (different URLs, no merge)', () => {
setModelData( model, '<$text linkHref="ckeditor.com">f[]oo$text>' );
model.change( writer => {
model.insertContent( writer.createText( 'INSERTED', { linkHref: 'http://INSERTED' } ) );
} );
expect( getModelData( model ) ).to.equal(
'' +
'<$text linkHref="ckeditor.com">f$text>' +
'<$text linkHref="http://INSERTED">INSERTED[]$text>' +
'<$text linkHref="ckeditor.com">oo$text>' +
''
);
expect( [ ...model.document.selection.getAttributeKeys() ] ).to.have.members( [ 'linkHref' ] );
} );
it( 'should not remove link atttributes when pasting before another link (different URLs, no merge)', () => {
setModelData( model, '[]<$text linkHref="ckeditor.com">foo$text>' );
expect( model.document.selection.isGravityOverridden ).to.be.false;
model.change( writer => {
model.insertContent( writer.createText( 'INSERTED', { linkHref: 'http://INSERTED' } ) );
} );
expect( getModelData( model ) ).to.equal(
'' +
'<$text linkHref="http://INSERTED">INSERTED[]$text>' +
'<$text linkHref="ckeditor.com">foo$text>' +
''
);
expect( [ ...model.document.selection.getAttributeKeys() ] ).to.have.members( [ 'linkHref' ] );
expect( model.document.selection.getAttribute( 'linkHref' ) ).to.equal( 'http://INSERTED' );
} );
} );
describe( 'command', () => {
it( 'should register link command', () => {
const command = editor.commands.get( 'link' );
expect( command ).to.be.instanceOf( LinkCommand );
} );
it( 'should register unlink command', () => {
const command = editor.commands.get( 'unlink' );
expect( command ).to.be.instanceOf( UnlinkCommand );
} );
} );
describe( 'data pipeline conversions', () => {
it( 'should convert `` to `linkHref="url"` attribute', () => {
editor.setData( 'foobar
' );
expect( getModelData( model, { withoutSelection: true } ) )
.to.equal( '<$text linkHref="url">foo$text>bar' );
expect( editor.getData() ).to.equal( 'foobar
' );
} );
it( 'should be integrated with autoparagraphing', () => {
editor.setData( 'foobar' );
expect( getModelData( model, { withoutSelection: true } ) )
.to.equal( '<$text linkHref="url">foo$text>bar' );
expect( editor.getData() ).to.equal( '
foobar
' );
} );
// https://github.com/ckeditor/ckeditor5/issues/500
it( 'should not pick up ``', () => {
editor.setData( 'foobar
' );
expect( getModelData( model, { withoutSelection: true } ) )
.to.equal( 'foobar' );
} );
// CKEditor 4 does. And CKEditor 5's balloon allows creating such links.
it( 'should pick up ``', () => {
editor.setData( 'foobar
' );
expect( getModelData( model, { withoutSelection: true } ) )
.to.equal( '<$text linkHref="">foo$text>bar' );
expect( editor.getData() ).to.equal( 'foobar
' );
} );
// The editor's role is not to filter out potentially malicious data.
// Its job is to not let this code be executed inside the editor (see the test in "editing pipeline conversion").
it( 'should output a link with a potential XSS code', () => {
setModelData( model, '[]' );
model.change( writer => {
writer.insertText( 'foo', { linkHref: 'javascript:alert(1)' }, model.document.selection.getFirstPosition() );
} );
expect( editor.getData() ).to.equal( 'foo
' );
} );
it( 'should load a link with a potential XSS code', () => {
editor.setData( 'foo
' );
expect( getModelData( model, { withoutSelection: true } ) )
.to.equal( '<$text linkHref="javascript:alert(1)">foo$text>' );
} );
} );
describe( 'editing pipeline conversion', () => {
it( 'should convert attribute', () => {
setModelData( model, '<$text linkHref="url">foo$text>bar' );
expect( getViewData( editor.editing.view, { withoutSelection: true } ) ).to.equal( 'foobar
' );
} );
it( 'should convert to link element instance', () => {
setModelData( model, '<$text linkHref="url">foo$text>bar' );
const element = editor.editing.view.document.getRoot().getChild( 0 ).getChild( 0 );
expect( isLinkElement( element ) ).to.be.true;
} );
// https://github.com/ckeditor/ckeditor5-link/issues/121
it( 'should should set priority for `linkHref` higher than all other attribute elements', () => {
model.schema.extend( '$text', { allowAttributes: 'foo' } );
editor.conversion.for( 'downcast' ).attributeToElement( { model: 'foo', view: 'f' } );
setModelData( model,
'' +
'<$text linkHref="url">a$text><$text foo="true" linkHref="url">b$text><$text linkHref="url">c$text>' +
'' );
expect( editor.getData() ).to.equal( 'abc
' );
} );
it( 'must not render a link with a potential XSS code', () => {
setModelData( model, '<$text linkHref="javascript:alert(1)">[]foo$text>bar[]' );
expect( getViewData( editor.editing.view, { withoutSelection: true } ) )
.to.equal( 'foobar
' );
} );
} );
describe( 'link highlighting', () => {
it( 'should convert the highlight to a proper view classes', () => {
setModelData( model,
'foo <$text linkHref="url">b{}ar$text> baz'
);
expect( model.document.selection.hasAttribute( 'linkHref' ) ).to.be.true;
expect( getViewData( view ) ).to.equal(
'foo b{}ar baz
'
);
} );
it( 'should work whenever selection has linkHref attribute - link start', () => {
setModelData( model,
'foo {}<$text linkHref="url">bar$text> baz'
);
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( getViewData( view ) ).to.equal(
'foo {}bar baz
'
);
} );
it( 'should work whenever selection has linkHref attribute - link end', () => {
setModelData( model,
'foo <$text linkHref="url">bar$text>{} baz'
);
expect( model.document.selection.hasAttribute( 'linkHref' ) ).to.be.true;
expect( getViewData( view ) ).to.equal(
'foo bar{} baz
'
);
} );
it( 'should render highlight correctly after splitting the link', () => {
setModelData( model,
'foo <$text linkHref="url">li{}nk$text> baz'
);
editor.execute( 'enter' );
expect( getModelData( model ) ).to.equal(
'foo <$text linkHref="url">li$text>' +
'<$text linkHref="url">[]nk$text> baz'
);
expect( model.document.selection.hasAttribute( 'linkHref' ) ).to.be.true;
} );
it( 'should remove classes when selection is moved out from the link', () => {
setModelData( model,
'foo <$text linkHref="url">li{}nk$text> baz'
);
expect( getViewData( view ) ).to.equal(
'foo li{}nk baz
'
);
model.change( writer => writer.setSelection( model.document.getRoot().getChild( 0 ), 0 ) );
expect( getViewData( view ) ).to.equal(
'{}foo link baz
'
);
} );
it( 'should work correctly when selection is moved inside link', () => {
setModelData( model,
'foo <$text linkHref="url">li{}nk$text> baz'
);
expect( getViewData( view ) ).to.equal(
'foo li{}nk baz
'
);
model.change( writer => writer.setSelection( model.document.getRoot().getChild( 0 ), 5 ) );
expect( getViewData( view ) ).to.equal(
'foo l{}ink baz
'
);
} );
describe( 'downcast conversion integration', () => {
it( 'works for the #insert event', () => {
setModelData( model,
'foo <$text linkHref="url">li{}nk$text> baz'
);
model.change( writer => {
writer.insertText( 'FOO', { linkHref: 'url' }, model.document.selection.getFirstPosition() );
} );
expect( getViewData( view ) ).to.equal(
'foo liFOO{}nk baz
'
);
} );
it( 'works for the #remove event', () => {
setModelData( model,
'foo <$text linkHref="url">li{}nk$text> baz'
);
model.change( writer => {
writer.remove( writer.createRange(
writer.createPositionAt( model.document.getRoot().getChild( 0 ), 0 ),
writer.createPositionAt( model.document.getRoot().getChild( 0 ), 5 )
) );
} );
expect( getViewData( view ) ).to.equal(
'i{}nk baz
'
);
} );
it( 'works for the #attribute event', () => {
setModelData( model,
'foo <$text linkHref="url">li{}nk$text> baz'
);
model.change( writer => {
writer.setAttribute( 'linkHref', 'new-url', writer.createRange(
model.document.selection.getFirstPosition().getShiftedBy( -1 ),
model.document.selection.getFirstPosition().getShiftedBy( 1 ) )
);
} );
expect( getViewData( view ) ).to.equal(
'foo li{}nk baz
'
);
} );
it( 'works for the #selection event', () => {
setModelData( model,
'foo <$text linkHref="url">li{}nk$text> baz'
);
model.change( writer => {
writer.setSelection( writer.createRange(
model.document.selection.getFirstPosition().getShiftedBy( -1 ),
model.document.selection.getFirstPosition().getShiftedBy( 1 ) )
);
} );
expect( getViewData( view ) ).to.equal(
'foo l{in}k baz
'
);
} );
it( 'works for the addMarker and removeMarker events', () => {
editor.conversion.for( 'editingDowncast' ).markerToHighlight( { model: 'fooMarker', view: {} } );
setModelData( model,
'foo <$text linkHref="url">li{}nk$text> baz'
);
model.change( writer => {
const range = writer.createRange(
writer.createPositionAt( model.document.getRoot().getChild( 0 ), 0 ),
writer.createPositionAt( model.document.getRoot().getChild( 0 ), 5 )
);
writer.addMarker( 'fooMarker', { range, usingOperation: true } );
} );
expect( getViewData( view ) ).to.equal(
'foo li{}nk baz
'
);
model.change( writer => writer.removeMarker( 'fooMarker' ) );
expect( getViewData( view ) ).to.equal(
'foo li{}nk baz
'
);
} );
} );
} );
describe( 'link attributes decorator', () => {
describe( 'default behavior', () => {
const testLinks = [
{
external: true,
url: 'http://example.com'
}, {
external: true,
url: 'https://cksource.com'
}, {
external: false,
url: 'ftp://server.io'
}, {
external: true,
url: '//schemaless.org'
}, {
external: false,
url: 'www.ckeditor.com'
}, {
external: false,
url: '/relative/url.html'
}, {
external: false,
url: 'another/relative/url.html'
}, {
external: false,
url: '#anchor'
}, {
external: false,
url: 'mailto:some@user.org'
}, {
external: false,
url: 'tel:123456789'
}
];
it( 'link.addTargetToExternalLinks is predefined as false value', () => {
expect( editor.config.get( 'link.addTargetToExternalLinks' ) ).to.be.false;
} );
describe( 'for link.addTargetToExternalLinks = false', () => {
let editor, model;
beforeEach( async () => {
editor = await ClassicTestEditor.create( element, {
plugins: [ Paragraph, LinkEditing, Enter ],
link: {
addTargetToExternalLinks: true
}
} );
model = editor.model;
view = editor.editing.view;
} );
afterEach( async () => {
await editor.destroy();
} );
it( 'link.addTargetToExternalLinks is set as true value', () => {
expect( editor.config.get( 'link.addTargetToExternalLinks' ) ).to.be.true;
} );
testLinks.forEach( link => {
it( `link: ${ link.url } should be treat as ${ link.external ? 'external' : 'non-external' } link`, () => {
editor.setData( `foobar
` );
expect( getModelData( model, { withoutSelection: true } ) )
.to.equal( `<$text linkHref="${ link.url }">foo$text>bar` );
if ( link.external ) {
expect( editor.getData() )
.to.equal( `foobar
` );
} else {
expect( editor.getData() ).to.equal( `foobar
` );
}
} );
} );
} );
testLinks.forEach( link => {
it( `link: ${ link.url } should not get 'target' and 'rel' attributes`, () => {
editor.setData( `foobar
` );
expect( getModelData( model, { withoutSelection: true } ) )
.to.equal( `<$text linkHref="${ link.url }">foo$text>bar` );
expect( editor.getData() ).to.equal( `foobar
` );
} );
} );
} );
describe( 'custom config', () => {
describe( 'mode: automatic', () => {
const testLinks = [
{
url: 'relative/url.html',
attributes: {}
}, {
url: 'http://exmaple.com',
attributes: {
target: '_blank'
}
}, {
url: 'https://example.com/download/link.pdf',
attributes: {
target: '_blank',
download: 'download'
}
}, {
url: 'mailto:some@person.io',
attributes: {
class: 'mail-url'
}
}
];
beforeEach( async () => {
await editor.destroy();
editor = await ClassicTestEditor.create( element, {
plugins: [ Paragraph, LinkEditing, Enter ],
link: {
addTargetToExternalLinks: false,
decorators: {
isExternal: {
mode: 'automatic',
callback: url => url.startsWith( 'http' ),
attributes: {
target: '_blank'
}
},
isDownloadable: {
mode: 'automatic',
callback: url => url.includes( 'download' ),
attributes: {
download: 'download'
}
},
isMail: {
mode: 'automatic',
callback: url => url.startsWith( 'mailto:' ),
attributes: {
class: 'mail-url'
}
}
}
}
} );
model = editor.model;
view = editor.editing.view;
} );
testLinks.forEach( link => {
it( `Link: ${ link.url } should get attributes: ${ JSON.stringify( link.attributes ) }`, () => {
const ORDER = [ 'target', 'download', 'class' ];
const attr = Object.entries( link.attributes ).sort( ( a, b ) => {
const aIndex = ORDER.indexOf( a[ 0 ] );
const bIndex = ORDER.indexOf( b[ 0 ] );
return aIndex - bIndex;
} );
const reducedAttr = attr.reduce( ( acc, cur ) => {
return acc + `${ cur[ 0 ] }="${ cur[ 1 ] }" `;
}, '' );
editor.setData( `foobar
` );
expect( getModelData( model, { withoutSelection: true } ) )
.to.equal( `<$text linkHref="${ link.url }">foo$text>bar` );
// Order of attributes is important, that's why this is assert is construct in such way.
expect( editor.getData() ).to.equal( `foobar
` );
} );
} );
} );
} );
describe( 'custom linkHref converter', () => {
beforeEach( async () => {
class CustomLinks extends Plugin {
init() {
const editor = this.editor;
editor.conversion.for( 'downcast' ).add( dispatcher => {
dispatcher.on( 'attribute:linkHref', ( evt, data, conversionApi ) => {
conversionApi.consumable.consume( data.item, 'attribute:linkHref' );
// Very simplified downcast just for test assertion.
const viewWriter = conversionApi.writer;
const linkElement = viewWriter.createAttributeElement(
'a',
{
href: data.attributeNewValue
}, {
priority: 5
}
);
viewWriter.setCustomProperty( 'link', true, linkElement );
viewWriter.wrap( conversionApi.mapper.toViewRange( data.range ), linkElement );
}, { priority: 'highest' } );
} );
}
}
await editor.destroy();
editor = await ClassicTestEditor.create( element, {
plugins: [ Paragraph, LinkEditing, Enter, CustomLinks ],
link: {
addTargetToExternalLinks: true
}
} );
model = editor.model;
view = editor.editing.view;
} );
it( 'has possibility to override default one', () => {
editor.setData( 'foobar
' );
expect( getModelData( model, { withoutSelection: true } ) )
.to.equal( '<$text linkHref="http://example.com">foo$text>bar' );
expect( editor.getData() ).to.equal( 'foobar
' );
} );
} );
describe( 'upcast converter', () => {
let element, editor;
beforeEach( () => {
element = document.createElement( 'div' );
document.body.appendChild( element );
} );
afterEach( () => {
element.remove();
} );
it( 'should upcast attributes from initial data', async () => {
editor = await ClassicTestEditor.create( element, {
initialData: 'Foo' +
'Bar
',
plugins: [ Paragraph, LinkEditing, Enter ],
link: {
decorators: {
isExternal: {
mode: 'manual',
label: 'Open in a new window',
attributes: {
target: '_blank',
rel: 'noopener noreferrer'
}
},
isDownloadable: {
mode: 'manual',
label: 'Downloadable',
attributes: {
download: 'file'
}
}
}
}
} );
model = editor.model;
expect( getModelData( model, { withoutSelection: true } ) ).to.equal(
'' +
'<$text linkHref="url" linkIsDownloadable="true" linkIsExternal="true">Foo$text>' +
'<$text linkHref="example.com" linkIsDownloadable="true">Bar$text>' +
''
);
await editor.destroy();
} );
it( 'should not upcast partial and incorrect attributes', async () => {
editor = await ClassicTestEditor.create( element, {
initialData: 'Foo' +
'Bar
',
plugins: [ Paragraph, LinkEditing, Enter ],
link: {
decorators: {
isExternal: {
mode: 'manual',
label: 'Open in a new window',
attributes: {
target: '_blank',
rel: 'noopener noreferrer'
}
},
isDownloadable: {
mode: 'manual',
label: 'Downloadable',
attributes: {
download: 'file'
}
}
}
}
} );
model = editor.model;
expect( getModelData( model, { withoutSelection: true } ) ).to.equal(
'' +
'<$text linkHref="url">Foo$text>' +
'<$text linkHref="example.com">Bar$text>' +
''
);
await editor.destroy();
} );
} );
} );
// https://github.com/ckeditor/ckeditor5/issues/1016
describe( 'typing around the link after a click', () => {
let editor;
beforeEach( async () => {
editor = await ClassicTestEditor.create( element, {
plugins: [ Paragraph, LinkEditing, Enter, Typing, BoldEditing ],
link: {
decorators: {
isFoo: {
mode: 'manual',
label: 'Foo',
attributes: {
class: 'foo'
}
},
isBar: {
mode: 'manual',
label: 'Bar',
attributes: {
target: '_blank'
}
}
}
}
} );
model = editor.model;
view = editor.editing.view;
} );
afterEach( async () => {
await editor.destroy();
} );
it( 'should insert content after the link', () => {
setModelData( model, '<$text linkHref="url">Bar[]$text>' );
editor.editing.view.document.fire( 'mousedown' );
editor.editing.view.document.fire( 'selectionChange', {
newSelection: view.document.selection
} );
expect( getModelData( model ) ).to.equal( '<$text linkHref="url">Bar$text>[]' );
editor.execute( 'input', { text: 'Foo' } );
expect( getModelData( model ) ).to.equal( '<$text linkHref="url">Bar$text>Foo[]' );
} );
it( 'should insert content before the link', () => {
setModelData( model, '<$text linkHref="url">[]Bar$text>' );
editor.editing.view.document.fire( 'mousedown' );
editor.editing.view.document.fire( 'selectionChange', {
newSelection: view.document.selection
} );
expect( getModelData( model ) ).to.equal( '[]<$text linkHref="url">Bar$text>' );
editor.execute( 'input', { text: 'Foo' } );
expect( getModelData( model ) ).to.equal( 'Foo[]<$text linkHref="url">Bar$text>' );
} );
it( 'should insert content to the link if clicked inside it', () => {
setModelData( model, '<$text linkHref="url">B[]ar$text>' );
editor.editing.view.document.fire( 'mousedown' );
editor.editing.view.document.fire( 'selectionChange', {
newSelection: view.document.selection
} );
expect( getModelData( model ) ).to.equal( '<$text linkHref="url">B[]ar$text>' );
editor.execute( 'input', { text: 'ar. B' } );
expect( getModelData( model ) ).to.equal( '<$text linkHref="url">Bar. B[]ar$text>' );
} );
it( 'should insert content between two links (selection at the end of the first link)', () => {
setModelData( model, '<$text linkHref="foo">Foo[]$text><$text linkHref="bar">Bar$text>' );
editor.editing.view.document.fire( 'mousedown' );
editor.editing.view.document.fire( 'selectionChange', {
newSelection: view.document.selection
} );
expect( getModelData( model ) ).to.equal(
'<$text linkHref="foo">Foo$text>[]<$text linkHref="bar">Bar$text>'
);
editor.execute( 'input', { text: 'Foo' } );
expect( getModelData( model ) ).to.equal(
'<$text linkHref="foo">Foo$text>Foo[]<$text linkHref="bar">Bar$text>'
);
} );
it( 'should insert content between two links (selection at the beginning of the second link)', () => {
setModelData( model, '<$text linkHref="foo">Foo$text><$text linkHref="bar">[]Bar$text>' );
editor.editing.view.document.fire( 'mousedown' );
editor.editing.view.document.fire( 'selectionChange', {
newSelection: view.document.selection
} );
expect( getModelData( model ) ).to.equal(
'<$text linkHref="foo">Foo$text>[]<$text linkHref="bar">Bar$text>'
);
editor.execute( 'input', { text: 'Foo' } );
expect( getModelData( model ) ).to.equal(
'<$text linkHref="foo">Foo$text>Foo[]<$text linkHref="bar">Bar$text>'
);
} );
it( 'should not touch other attributes than `linkHref`', () => {
setModelData( model, '<$text bold="true" linkHref="url">Bar[]$text>' );
editor.editing.view.document.fire( 'mousedown' );
editor.editing.view.document.fire( 'selectionChange', {
newSelection: view.document.selection
} );
expect( getModelData( model ) ).to.equal(
'<$text bold="true" linkHref="url">Bar$text><$text bold="true">[]$text>'
);
editor.execute( 'input', { text: 'Foo' } );
expect( getModelData( model ) ).to.equal(
'<$text bold="true" linkHref="url">Bar$text><$text bold="true">Foo[]$text>'
);
} );
it( 'should do nothing if the text was not clicked', () => {
setModelData( model, '<$text linkHref="url">Bar[]$text>' );
editor.editing.view.document.fire( 'selectionChange', {
newSelection: view.document.selection
} );
expect( getModelData( model ) ).to.equal( '<$text linkHref="url">Bar[]$text>' );
} );
it( 'should do nothing if the selection is not collapsed after the click', () => {
setModelData( model, '[<$text linkHref="url">Bar$text>]' );
editor.editing.view.document.fire( 'mousedown' );
editor.editing.view.document.fire( 'selectionChange', {
newSelection: view.document.selection
} );
expect( getModelData( model ) ).to.equal( '[<$text linkHref="url">Bar$text>]' );
} );
it( 'should do nothing if the text is not a link', () => {
setModelData( model, '<$text bold="true">Bar[]$text>' );
editor.editing.view.document.fire( 'mousedown' );
editor.editing.view.document.fire( 'selectionChange', {
newSelection: view.document.selection
} );
expect( getModelData( model ) ).to.equal( '<$text bold="true">Bar[]$text>' );
} );
it( 'should remove manual decorators', () => {
model.schema.extend( '$text', {
allowIn: '$root',
allowAttributes: [ 'linkIsFoo', 'linkIsBar' ]
} );
setModelData( model, '<$text linkIsFoo="true" linkIsBar="true" linkHref="url">Bar[]$text>' );
editor.editing.view.document.fire( 'mousedown' );
editor.editing.view.document.fire( 'selectionChange', {
newSelection: view.document.selection
} );
expect( getModelData( model ) ).to.equal(
'<$text linkHref="url" linkIsBar="true" linkIsFoo="true">Bar$text>[]'
);
editor.execute( 'input', { text: 'Foo' } );
expect( getModelData( model ) ).to.equal(
'<$text linkHref="url" linkIsBar="true" linkIsFoo="true">Bar$text>Foo[]'
);
} );
} );
} );