Skip to content

Commit

Permalink
Merge pull request #7370 from ckeditor/i/4858
Browse files Browse the repository at this point in the history
Feature (link): Introduced the `config.link.defaultProtocol` option for automatically adding it to the links when it's not provided by the user in the link form. Closes #4858.
  • Loading branch information
jodator authored Jun 10, 2020
2 parents b127f41 + e5bf396 commit 76c762e
Show file tree
Hide file tree
Showing 9 changed files with 315 additions and 8 deletions.
24 changes: 24 additions & 0 deletions packages/ckeditor5-link/docs/features/link.md
Original file line number Diff line number Diff line change
Expand Up @@ -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( ... );
```

<info-box>
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`.
</info-box>

#### 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.
Expand Down
1 change: 1 addition & 0 deletions packages/ckeditor5-link/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
22 changes: 22 additions & 0 deletions packages/ckeditor5-link/src/link.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 `//`.
Expand Down
17 changes: 15 additions & 2 deletions packages/ckeditor5-link/src/linkui.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <kbd>Ctrl+K</kbd> keystroke.
Expand Down Expand Up @@ -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' );

Expand All @@ -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();
} );

Expand Down
11 changes: 6 additions & 5 deletions packages/ckeditor5-link/src/ui/linkformview.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
Expand Down Expand Up @@ -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;
}
Expand Down
146 changes: 145 additions & 1 deletion packages/ckeditor5-link/tests/linkui.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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' );
} );
Expand All @@ -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, '<paragraph>f[o]o</paragraph>' );
Expand Down
59 changes: 59 additions & 0 deletions packages/ckeditor5-link/tests/manual/protocol.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<style>
code {
background: hsl(0, 0%, 90%);
}

sup {
position: relative;
}

.ck-editor__editable sup:after {
content: '';
display: inline-block;
position: absolute;
width: 14px;
height: 14px;
left: 1px;
top: 1px;
background: hsla(207, 87%, 55%, 0.5);
border-radius: 50px;
animation: pulse 2s linear infinite;
}

@keyframes pulse {
0% {
transform: scale(1);
}

50% {
transform: scale(2);
}

100% {
transform: scale(1);
}
}
</style>


<h2><code>Feature is disabled</code></h2>
<div id="editor0">
<p>This is <a href="http://ckeditor.com">CKEditor5</a> from <a href="http://cksource.com">CKSource</a>. If you need more information please contact us at support@example.com.</p>
</div>

<h2><code>http://</code></h2>
<div id="editor1">
<p>This is <a href="http://ckeditor.com">CKEditor5</a> from <a href="http://cksource.com">CKSource</a>. If you need more information please contact us at support@example.com <sup class="indicator">[1]</sup>.</p>
</div>

<p><sup>[1]</sup><strong>When feature enabled:</strong> copy the email address and create a link with it (<code>mailto:</code> protocol will be added automatically).</p>

<h2><code>https://</code></h2>
<div id="editor2">
<p>This is <a href="http://ckeditor.com">CKEditor5</a> from <a href="http://cksource.com">CKSource</a>. If you need more information please contact us at support@example.com.</p>
</div>

<h2><code>mailto:</code></h2>
<div id="editor3">
<p>This is <a href="http://ckeditor.com">CKEditor5</a> from <a href="http://cksource.com">CKSource</a>. If you need more information please contact us at support@example.com.</p>
</div>
Loading

0 comments on commit 76c762e

Please sign in to comment.