diff --git a/CHANGES.md b/CHANGES.md
index 7f508f0b8c2..bfdf6b483e7 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -3,6 +3,10 @@
## CKEditor 4.10
+New Features:
+
+* [#1761](https://github.com/ckeditor/ckeditor-dev/issues/1761): [Autolink](https://ckeditor.com/cke4/addon/autolink) plugin supports email links.
+
Fixed Issues:
* [#1458](https://github.com/ckeditor/ckeditor-dev/issues/1458): [Edge] Fixed: After blurring editor it takes 2 clicks to focus a widget.
diff --git a/plugins/autolink/plugin.js b/plugins/autolink/plugin.js
index 732dc690fb6..5e19551eba0 100644
--- a/plugins/autolink/plugin.js
+++ b/plugins/autolink/plugin.js
@@ -8,6 +8,8 @@
// Regex by Imme Emosol.
var validUrlRegex = /^(https?|ftp):\/\/(-\.)?([^\s\/?\.#]+\.?)+(\/[^\s]*)?[^\s\.,]$/ig,
+ // Regex by (https://www.w3.org/TR/html5/forms.html#valid-e-mail-address).
+ validEmailRegex = /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/g,
doubleQuoteRegex = /"/g;
CKEDITOR.plugins.add( 'autolink', {
@@ -26,8 +28,14 @@
return;
}
- // https://dev.ckeditor.com/ticket/13419
- data = data.replace( validUrlRegex , '$&' );
+ // Create valid email links (#1761).
+ if ( data.match( validEmailRegex ) ) {
+ data = data.replace( validEmailRegex, '$&' );
+ data = tryToEncodeLink( data );
+ } else {
+ // https://dev.ckeditor.com/ticket/13419
+ data = data.replace( validUrlRegex , '$&' );
+ }
// If link was discovered, change the type to 'html'. This is important e.g. when pasting plain text in Chrome
// where real type is correctly recognized.
@@ -37,6 +45,25 @@
evt.data.dataValue = data;
} );
+
+ function tryToEncodeLink( data ) {
+ // If enabled use link plugin to encode email link.
+ if ( editor.plugins.link ) {
+ var link = CKEDITOR.dom.element.createFromHtml( data ),
+ linkData = CKEDITOR.plugins.link.parseLinkAttributes( editor, link ),
+ attributes = CKEDITOR.plugins.link.getLinkAttributes( editor, linkData );
+
+ if ( !CKEDITOR.tools.isEmpty( attributes.set ) ) {
+ link.setAttributes( attributes.set );
+ }
+
+ if ( attributes.removed.length ) {
+ link.removeAttributes( attributes.removed );
+ }
+ return link.getOuterHtml();
+ }
+ return data;
+ }
}
} );
} )();
diff --git a/tests/plugins/autolink/autolink.js b/tests/plugins/autolink/autolink.js
index 8242d63817f..f3f5ea85bf3 100644
--- a/tests/plugins/autolink/autolink.js
+++ b/tests/plugins/autolink/autolink.js
@@ -5,28 +5,34 @@
/* bender-include: ../clipboard/_helpers/pasting.js */
/* global assertPasteEvent */
-bender.editor = {
- config: {
- allowedContent: true,
- pasteFilter: null
+bender.editors = {
+ classic: {
+ config: {
+ allowedContent: true,
+ pasteFilter: null
+ }
+ },
+ encodedDefault: {
+ config: {
+ allowedContent: true,
+ pasteFilter: null,
+ emailProtection: 'encode',
+ extraPlugins: 'link'
+ }
+ },
+ encodedCustom: {
+ config: {
+ allowedContent: true,
+ pasteFilter: null,
+ emailProtection: 'mt(NAME,DOMAIN,SUBJECT,BODY)',
+ extraPlugins: 'link'
+ }
}
};
bender.test( {
- 'test normal link': function() {
- var pastedText = 'https://placekitten.com/g/180/300',
- expected = '' + pastedText + '';
-
- assertPasteEvent( this.editor, { dataValue: pastedText }, { dataValue: expected, type: 'html' } );
- },
- 'test fake link': function() {
- var pastedText = 'https//placekitten.com/g/190/300';
-
- assertPasteEvent( this.editor, { dataValue: pastedText }, { dataValue: pastedText, type: 'html' } );
- },
-
- 'test link with HTML tags': function() {
+ 'test URL link with HTML tags': function() {
var pastedTexts = [
'https://
placekitten.com/g/200/301',
'https://
placekitten.com/g/200/302',
@@ -37,43 +43,76 @@ bender.test( {
var pastedText;
while ( ( pastedText = pastedTexts.pop() ) ) {
- this.editor.once( 'paste', function( evt ) {
+ this.editors.classic.once( 'paste', function( evt ) {
+ evt.cancel();
+
+ assert.areSame( -1, evt.data.dataValue.search( /mail@example.com',
+ '
mail@example.com',
+ '
mail@example.com',
+ 'mail@example.com'
+ ];
+
+ var pastedText;
+
+ while ( ( pastedText = pastedTexts.pop() ) ) {
+ this.editors.classic.once( 'paste', function( evt ) {
evt.cancel();
assert.areSame( -1, evt.data.dataValue.search( /' + pastedText + '', evt.data.dataValue );
}, null, null, 900 );
- this.editor.execCommand( 'paste', pastedText );
+ this.editors.classic.execCommand( 'paste', pastedText );
+ }
+ },
+
+ 'test various valid email links': function() {
+ var pastedTexts = [
+ 'mail@example.com',
+ 'mail@mail',
+ ".!#$%&'*+-/=?^_`{|}~@1234567890",
+ 'mail@192.168.20.99'
+ ];
+
+ var pastedText;
+
+ while ( ( pastedText = pastedTexts.pop() ) ) {
+ this.editors.classic.once( 'paste', function( evt ) {
+ evt.cancel();
+
+ assert.areSame( '' + pastedText + '', evt.data.dataValue );
+ }, null, null, 900 );
+
+ this.editors.classic.execCommand( 'paste', pastedText );
}
},
- 'test various invalid links': function() {
+ 'test various invalid URL links': function() {
var pastedTexts = [
+ 'https//placekitten.com/g/190/300',
'https://placekitten.com/g/181/300.',
'http://giphy.com?search',
'https://www.google.pl,,,,',
@@ -106,64 +167,115 @@ bender.test( {
var pastedText;
while ( ( pastedText = pastedTexts.pop() ) ) {
- this.editor.once( 'paste', function( evt ) {
+ this.editors.classic.once( 'paste', function( evt ) {
+ evt.cancel();
+
+ assert.areSame( pastedText, evt.data.dataValue );
+ }, null, null, 900 );
+
+ this.editors.classic.execCommand( 'paste', pastedText );
+ }
+ },
+
+ 'test various invalid email links': function() {
+ var pastedTexts = [
+ 'mail@',
+ '@mail',
+ 'hello,world@host.com',
+ 'example@hello,world',
+ '"@emai.com',
+ '"example"@email.com'
+ ];
+
+ var pastedText;
+
+ while ( ( pastedText = pastedTexts.pop() ) ) {
+ this.editors.classic.once( 'paste', function( evt ) {
evt.cancel();
assert.areSame( pastedText, evt.data.dataValue );
}, null, null, 900 );
- this.editor.execCommand( 'paste', pastedText );
+ this.editors.classic.execCommand( 'paste', pastedText );
}
},
- 'test pasting multiple links': function() {
+ 'test pasting multiple URL links': function() {
var pastedText = 'http://en.wikipedia.org/wiki/Weasel http://en.wikipedia.org/wiki/Weasel';
- assertPasteEvent( this.editor, { dataValue: pastedText }, { dataValue: pastedText, type: 'html' } );
+ assertPasteEvent( this.editors.classic, { dataValue: pastedText }, { dataValue: pastedText, type: 'html' } );
+ },
+
+ 'test pasting multiple email links': function() {
+ var pastedText = 'example1@mail.com example2@mail.com';
+
+ assertPasteEvent( this.editors.classic, { dataValue: pastedText }, { dataValue: pastedText, type: 'html' } );
},
'test pasting whole paragraph': function() {
var pastedText =
'A multi-channel operating strategy' +
'drives the enabler, while the enablers strategically embrace the game-changing, ' +
- 'organic and cross-enterprise cultures.';
+ 'organic and cross-enterprise ' +
+ 'cultures.';
- assertPasteEvent( this.editor, { dataValue: pastedText }, { dataValue: pastedText, type: 'html' } );
+ assertPasteEvent( this.editors.classic, { dataValue: pastedText }, { dataValue: pastedText, type: 'html' } );
},
'test content that is a link': function() {
var pastedText = 'Weasel';
- assertPasteEvent( this.editor, { dataValue: pastedText }, { dataValue: pastedText, type: 'html' } );
+ assertPasteEvent( this.editors.classic, { dataValue: pastedText }, { dataValue: pastedText, type: 'html' } );
},
'test type is changed once a link is created': function() {
var pastedText = 'https://placekitten.com/g/180/300',
expected = '' + pastedText + '';
- assertPasteEvent( this.editor, { dataValue: pastedText, type: 'text' }, { dataValue: expected, type: 'html' } );
+ assertPasteEvent( this.editors.classic, { dataValue: pastedText, type: 'text' }, { dataValue: expected, type: 'html' } );
},
'test type is not changed if link was not found': function() {
var pastedText = 'foo bar';
- assertPasteEvent( this.editor, { dataValue: pastedText, type: 'text' }, { dataValue: pastedText, type: 'text' } );
+ assertPasteEvent( this.editors.classic, { dataValue: pastedText, type: 'text' }, { dataValue: pastedText, type: 'text' } );
},
'test internal paste is not autolinked': function() {
- var editor = this.editor,
+ var editor = this.editors.classic,
pastedText = 'https://foo.bar/g/185/310';
- this.editor.once( 'paste', function( evt ) {
+ this.editors.classic.once( 'paste', function( evt ) {
evt.data.dataTransfer.sourceEditor = editor;
}, null, null, 1 );
- this.editor.once( 'paste', function( evt ) {
+ this.editors.classic.once( 'paste', function( evt ) {
evt.cancel();
assert.areSame( pastedText, evt.data.dataValue );
}, null, null, 900 );
- this.editor.execCommand( 'paste', pastedText );
+ this.editors.classic.execCommand( 'paste', pastedText );
+ },
+
+ 'test created protected mail link (function)': function() {
+ var pastedText = 'a@a',
+ expected = 'a@a';
+
+ assertPasteEvent( this.editors.encodedDefault, { dataValue: pastedText, type: 'text' }, function( data ) {
+ assert.areEqual( 'html', data.type );
+ assert.areEqual( expected, bender.tools.compatHtml( data.dataValue ) );
+ } );
+ },
+
+ 'test created protected mail link (string)': function() {
+ var pastedText = 'a@a',
+ expected = 'a@a';
+
+ assertPasteEvent( this.editors.encodedCustom, { dataValue: pastedText, type: 'text' }, function( data ) {
+ assert.areEqual( 'html', data.type );
+ assert.areEqual( expected, bender.tools.compatHtml( data.dataValue ) );
+ } );
}
} );
diff --git a/tests/plugins/autolink/manual/mail.html b/tests/plugins/autolink/manual/mail.html
new file mode 100644
index 00000000000..b172c879374
--- /dev/null
+++ b/tests/plugins/autolink/manual/mail.html
@@ -0,0 +1,25 @@
+