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]' + ); + + 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]' + ); + + 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.