diff --git a/packages/ckeditor5-link/docs/features/link.md b/packages/ckeditor5-link/docs/features/link.md
index 4aa62b8189d..66ecf68396d 100644
--- a/packages/ckeditor5-link/docs/features/link.md
+++ b/packages/ckeditor5-link/docs/features/link.md
@@ -147,6 +147,30 @@ ClassicEditor
.catch( ... );
```
+#### Adding default link protocol for the external links
+
+Default link protocol can be usefull when user forget to type a full URL address to an external source, site etc. Sometimes copying the text, like for example `ckeditor.com` and converting it to a link may cause some issues. When you do this, the created link will direct you to `yourdomain.com/ckeditor.com`, because you forgot to pass the right protocol which makes the link relative to the site where it appears.
+
+Enabling the `{@link module:link/link~LinkConfig#defaultProtocol config.link.defaultProtocol}`, the {@link module:link/link~Link} feature will handle this issue for you. By default it doesn't fix the passed link value, but when you set `{@link module:link/link~LinkConfig#defaultProtocol config.link.defaultProtocol}` to — for example — `http://`, the plugin will add the given protocol to the every link that may need it (like `ckeditor.com`, `example.com` etc. where `[protocol://]example.com` is missing). Here's the basic configuration example:
+
+```js
+ClassicEditor
+ .create( document.querySelector( '#editor' ), {
+ // ...
+ link: {
+ defaultProtocol: 'http://'
+ }
+ } )
+ .then( ... )
+ .catch( ... );
+```
+
+
+ Having `config.link.defaultProtocol` enabled you are still able to link things locally using `#` or `/`. Protocol won't be added to those links.
+
+ Enabled feature also gives you an **email addresses auto-detection** feature. When you submit `hello@example.com`, the plugin will change it automatically to `mailto:hello@example.com`.
+
+
#### Adding attributes to links based on pre–defined rules (automatic decorators)
Automatic link decorators match all links in the editor content against a {@link module:link/link~LinkDecoratorAutomaticDefinition function} which decides whether the link should receive some set of attributes, considering the URL (`href`) of the link. These decorators work silently and are being applied during the {@link framework/guides/architecture/editing-engine#conversion data downcast} only.
diff --git a/packages/ckeditor5-link/package.json b/packages/ckeditor5-link/package.json
index caa33e9d9bd..311dd9e7a22 100644
--- a/packages/ckeditor5-link/package.json
+++ b/packages/ckeditor5-link/package.json
@@ -16,6 +16,7 @@
"lodash-es": "^4.17.15"
},
"devDependencies": {
+ "@ckeditor/ckeditor5-basic-styles": "^19.0.1",
"@ckeditor/ckeditor5-block-quote": "^19.0.1",
"@ckeditor/ckeditor5-clipboard": "^19.0.1",
"@ckeditor/ckeditor5-editor-classic": "^19.0.1",
diff --git a/packages/ckeditor5-link/src/link.js b/packages/ckeditor5-link/src/link.js
index 2fa90aabe23..96f8e9f9897 100644
--- a/packages/ckeditor5-link/src/link.js
+++ b/packages/ckeditor5-link/src/link.js
@@ -57,6 +57,28 @@ export default class Link extends Plugin {
* @interface LinkConfig
*/
+/**
+ * When set, the editor will add the given protocol to the link when the user creates a link without one.
+ * For example, when the user is creating a link and types `ckeditor.com` in the link form input — during link submission —
+ * the editor will automatically add the `http://` protocol, so the link will be as follows: `http://ckeditor.com`.
+ *
+ * The feature also comes with an email auto-detection. When you submit `hello@example.com`
+ * the plugin will automatically change it to `mailto:hello@example.com`.
+ *
+ * ClassicEditor
+ * .create( editorElement, {
+ * link: {
+ * defaultProtocol: 'http://'
+ * }
+ * } )
+ * .then( ... )
+ * .catch( ... );
+ *
+ * **NOTE:** In case no configuration is provided, the editor won't auto-fix the links.
+ *
+ * @member {String} module:link/link~LinkConfig#defaultProtocol
+ */
+
/**
* When set to `true`, the `target="blank"` and `rel="noopener noreferrer"` attributes are automatically added to all external links
* in the editor. "External links" are all links in the editor content starting with `http`, `https`, or `//`.
diff --git a/packages/ckeditor5-link/src/linkui.js b/packages/ckeditor5-link/src/linkui.js
index 68370115999..1e78a65f05e 100644
--- a/packages/ckeditor5-link/src/linkui.js
+++ b/packages/ckeditor5-link/src/linkui.js
@@ -21,6 +21,8 @@ import LinkActionsView from './ui/linkactionsview';
import linkIcon from '../theme/icons/link.svg';
const linkKeystroke = 'Ctrl+K';
+const protocolRegExp = /^((\w+:(\/{2,})?)|(\W))/i;
+const emailRegExp = /[\w-]+@[\w-]+\.+[\w-]+/i;
/**
* The link UI plugin. It introduces the `'link'` and `'unlink'` buttons and support for the Ctrl+K keystroke.
@@ -143,8 +145,9 @@ export default class LinkUI extends Plugin {
_createFormView() {
const editor = this.editor;
const linkCommand = editor.commands.get( 'link' );
+ const defaultProtocol = editor.config.get( 'link.defaultProtocol' );
- const formView = new LinkFormView( editor.locale, linkCommand );
+ const formView = new LinkFormView( editor.locale, linkCommand, defaultProtocol );
formView.urlInputView.fieldView.bind( 'value' ).to( linkCommand, 'value' );
@@ -154,7 +157,17 @@ export default class LinkUI extends Plugin {
// Execute link command after clicking the "Save" button.
this.listenTo( formView, 'submit', () => {
- editor.execute( 'link', formView.urlInputView.fieldView.element.value, formView.getDecoratorSwitchesState() );
+ const { value } = formView.urlInputView.fieldView.element;
+
+ // The regex checks for the protocol syntax ('xxxx://' or 'xxxx:')
+ // or non-word charecters at the begining of the link ('/', '#' etc.).
+ const isProtocolNeeded = !!defaultProtocol && !protocolRegExp.test( value );
+ const isEmail = emailRegExp.test( value );
+
+ const protocol = isEmail ? 'mailto:' : defaultProtocol;
+ const parsedValue = value && isProtocolNeeded ? protocol + value : value;
+
+ editor.execute( 'link', parsedValue, formView.getDecoratorSwitchesState() );
this._closeFormView();
} );
diff --git a/packages/ckeditor5-link/src/ui/linkformview.js b/packages/ckeditor5-link/src/ui/linkformview.js
index 2b050ee8e25..e20a747e8d0 100644
--- a/packages/ckeditor5-link/src/ui/linkformview.js
+++ b/packages/ckeditor5-link/src/ui/linkformview.js
@@ -40,8 +40,9 @@ export default class LinkFormView extends View {
*
* @param {module:utils/locale~Locale} [locale] The localization services instance.
* @param {module:link/linkcommand~LinkCommand} linkCommand Reference to {@link module:link/linkcommand~LinkCommand}.
+ * @param {String} [protocol] A value of a protocol to be displayed in the input's placeholder.
*/
- constructor( locale, linkCommand ) {
+ constructor( locale, linkCommand, protocol ) {
super( locale );
const t = locale.t;
@@ -67,7 +68,7 @@ export default class LinkFormView extends View {
*
* @member {module:ui/labeledfield/labeledfieldview~LabeledFieldView}
*/
- this.urlInputView = this._createUrlInput();
+ this.urlInputView = this._createUrlInput( protocol );
/**
* The Save button view.
@@ -207,15 +208,15 @@ export default class LinkFormView extends View {
* Creates a labeled input view.
*
* @private
+ * @param {String} [protocol=http://] A value of a protocol to be displayed in the input's placeholder.
* @returns {module:ui/labeledfield/labeledfieldview~LabeledFieldView} Labeled field view instance.
*/
- _createUrlInput() {
+ _createUrlInput( protocol = 'https://' ) {
const t = this.locale.t;
-
const labeledInput = new LabeledFieldView( this.locale, createLabeledInputText );
labeledInput.label = t( 'Link URL' );
- labeledInput.fieldView.placeholder = 'https://example.com';
+ labeledInput.fieldView.placeholder = protocol + 'example.com';
return labeledInput;
}
diff --git a/packages/ckeditor5-link/tests/linkui.js b/packages/ckeditor5-link/tests/linkui.js
index 48c6b18f511..1f09e43f672 100644
--- a/packages/ckeditor5-link/tests/linkui.js
+++ b/packages/ckeditor5-link/tests/linkui.js
@@ -8,7 +8,7 @@
import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
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 { setData as setModelData, getData as getModelData } 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';
@@ -891,6 +891,27 @@ describe( 'LinkUI', () => {
describe( 'link form view', () => {
let focusEditableSpy;
+ const createEditorWithDefaultProtocol = defaultProtocol => {
+ return ClassicTestEditor
+ .create( editorElement, {
+ plugins: [ LinkEditing, LinkUI, Paragraph, BlockQuote ],
+ link: { defaultProtocol }
+ } )
+ .then( editor => {
+ const linkUIFeature = editor.plugins.get( LinkUI );
+ const formView = linkUIFeature.formView;
+
+ formView.render();
+
+ editor.model.schema.extend( '$text', {
+ allowIn: '$root',
+ allowAttributes: 'linkHref'
+ } );
+
+ return { editor, formView };
+ } );
+ };
+
beforeEach( () => {
focusEditableSpy = testUtils.sinon.spy( editor.editing.view, 'focus' );
} );
@@ -905,6 +926,129 @@ describe( 'LinkUI', () => {
expect( editor.ui.focusTracker.isFocused ).to.be.true;
} );
+ describe( 'link protocol', () => {
+ it( 'should use a default link protocol from the `config.link.defaultProtocol` when provided', () => {
+ return ClassicTestEditor
+ .create( editorElement, {
+ link: {
+ defaultProtocol: 'https://'
+ }
+ } )
+ .then( editor => {
+ const defaultProtocol = editor.config.get( 'link.defaultProtocol' );
+
+ expect( defaultProtocol ).to.equal( 'https://' );
+
+ editor.destroy();
+ } );
+ } );
+
+ it( 'should not add a protocol without the configuration', () => {
+ formView.urlInputView.fieldView.value = 'ckeditor.com';
+ formView.fire( 'submit' );
+
+ expect( formView.urlInputView.fieldView.value ).to.equal( 'ckeditor.com' );
+ } );
+
+ it( 'should not add a protocol to the local links even when `config.link.defaultProtocol` configured', () => {
+ return createEditorWithDefaultProtocol( 'http://' ).then( ( { editor, formView } ) => {
+ formView.urlInputView.fieldView.value = '#test';
+ formView.fire( 'submit' );
+
+ expect( formView.urlInputView.fieldView.value ).to.equal( '#test' );
+
+ editor.destroy();
+ } );
+ } );
+
+ it( 'should not add a protocol to the relative links even when `config.link.defaultProtocol` configured', () => {
+ return createEditorWithDefaultProtocol( 'http://' ).then( ( { editor, formView } ) => {
+ formView.urlInputView.fieldView.value = '/test.html';
+ formView.fire( 'submit' );
+
+ expect( formView.urlInputView.fieldView.value ).to.equal( '/test.html' );
+
+ editor.destroy();
+ } );
+ } );
+
+ it( 'should not add a protocol when given provided within the value even when `config.link.defaultProtocol` configured', () => {
+ return createEditorWithDefaultProtocol( 'http://' ).then( ( { editor, formView } ) => {
+ formView.urlInputView.fieldView.value = 'http://example.com';
+ formView.fire( 'submit' );
+
+ expect( formView.urlInputView.fieldView.value ).to.equal( 'http://example.com' );
+
+ editor.destroy();
+ } );
+ } );
+
+ it( 'should use the "http://" protocol when it\'s configured', () => {
+ return createEditorWithDefaultProtocol( 'http://' ).then( ( { editor, formView } ) => {
+ formView.urlInputView.fieldView.value = 'ckeditor.com';
+ formView.fire( 'submit' );
+
+ expect( formView.urlInputView.fieldView.value ).to.equal( 'http://ckeditor.com' );
+
+ editor.destroy();
+ } );
+ } );
+
+ it( 'should use the "http://" protocol when it\'s configured and form input value contains "www."', () => {
+ return createEditorWithDefaultProtocol( 'http://' ).then( ( { editor, formView } ) => {
+ formView.urlInputView.fieldView.value = 'www.ckeditor.com';
+ formView.fire( 'submit' );
+
+ expect( formView.urlInputView.fieldView.value ).to.equal( 'http://www.ckeditor.com' );
+
+ editor.destroy();
+ } );
+ } );
+
+ it( 'should propagate the protocol to the link\'s `linkHref` attribute in model', () => {
+ return createEditorWithDefaultProtocol( 'http://' ).then( ( { editor, formView } ) => {
+ setModelData( editor.model, '[ckeditor.com]' );
+
+ formView.urlInputView.fieldView.value = 'ckeditor.com';
+ formView.fire( 'submit' );
+
+ expect( getModelData( editor.model ) ).to.equal(
+ '[<$text linkHref="http://ckeditor.com">ckeditor.com$text>]'
+ );
+
+ editor.destroy();
+ } );
+ } );
+
+ it( 'should detect an email on submitting the form and add "mailto:" protocol automatically to the provided value', () => {
+ return createEditorWithDefaultProtocol( 'http://' ).then( ( { editor, formView } ) => {
+ setModelData( editor.model, '[email@example.com]' );
+
+ formView.urlInputView.fieldView.value = 'email@example.com';
+ formView.fire( 'submit' );
+
+ expect( formView.urlInputView.fieldView.value ).to.equal( 'mailto:email@example.com' );
+ expect( getModelData( editor.model ) ).to.equal(
+ '[<$text linkHref="mailto:email@example.com">email@example.com$text>]'
+ );
+
+ editor.destroy();
+ } );
+ } );
+
+ it( 'should not add an email protocol when given provided within the value' +
+ 'even when `config.link.defaultProtocol` configured', () => {
+ return createEditorWithDefaultProtocol( 'mailto:' ).then( ( { editor, formView } ) => {
+ formView.urlInputView.fieldView.value = 'mailto:test@example.com';
+ formView.fire( 'submit' );
+
+ expect( formView.urlInputView.fieldView.value ).to.equal( 'mailto:test@example.com' );
+
+ editor.destroy();
+ } );
+ } );
+ } );
+
describe( 'binding', () => {
beforeEach( () => {
setModelData( editor.model, 'f[o]o' );
diff --git a/packages/ckeditor5-link/tests/manual/protocol.html b/packages/ckeditor5-link/tests/manual/protocol.html
new file mode 100644
index 00000000000..1a2d083bcb5
--- /dev/null
+++ b/packages/ckeditor5-link/tests/manual/protocol.html
@@ -0,0 +1,59 @@
+
+
+
+
Feature is disabled
+
+
This is CKEditor5 from CKSource. If you need more information please contact us at support@example.com.
+
+
+http://
+
+
This is CKEditor5 from CKSource. If you need more information please contact us at support@example.com [1].
+
+
+[1]When feature enabled: copy the email address and create a link with it (mailto:
protocol will be added automatically).
+
+https://
+
+
This is CKEditor5 from CKSource. If you need more information please contact us at support@example.com.
+
+
+mailto:
+
+
This is CKEditor5 from CKSource. If you need more information please contact us at support@example.com.
+
diff --git a/packages/ckeditor5-link/tests/manual/protocol.js b/packages/ckeditor5-link/tests/manual/protocol.js
new file mode 100644
index 00000000000..9f33f4b3409
--- /dev/null
+++ b/packages/ckeditor5-link/tests/manual/protocol.js
@@ -0,0 +1,37 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* 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';
+import Superscript from '@ckeditor/ckeditor5-basic-styles/src/superscript';
+
+createEditorWithDefaultProtocol( '#editor0' );
+createEditorWithDefaultProtocol( '#editor1', 'http://' );
+createEditorWithDefaultProtocol( '#editor2', 'https://' );
+createEditorWithDefaultProtocol( '#editor3', 'mailto:' );
+
+function createEditorWithDefaultProtocol( editor, defaultProtocol ) {
+ return ClassicEditor
+ .create( document.querySelector( editor ), {
+ plugins: [ Link, Typing, Paragraph, Undo, Enter, Superscript ],
+ toolbar: [ 'link', 'undo', 'redo' ],
+ link: {
+ addTargetToExternalLinks: true,
+ ...defaultProtocol && { defaultProtocol }
+ }
+ } )
+ .then( editor => {
+ window.editor = editor;
+ } )
+ .catch( err => {
+ console.error( err.stack );
+ } );
+}
diff --git a/packages/ckeditor5-link/tests/manual/protocol.md b/packages/ckeditor5-link/tests/manual/protocol.md
new file mode 100644
index 00000000000..5c4f80f84fe
--- /dev/null
+++ b/packages/ckeditor5-link/tests/manual/protocol.md
@@ -0,0 +1,6 @@
+## Link protocol
+
+This test checks whether:
+- `config.link.defaultProtocol` applies.
+- when input value starts with a protocol-like syntax (like `http://` etc.) or any non-word (like `#` or `/`) then `defaultProtocol` won't be applied.
+- the plugin dynamically change link protocol to `mailto:` when email address was detected.