Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add validation to the link form #15633

Merged
merged 15 commits into from
Jan 8, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/_snippets/framework/ui/ui-balloon.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ import {
addToolbarToDropdown,
createDropdown,
createLabeledInputNumber,
createLabeledInputText
createLabeledInputText,
createLabeledInputUrl
} from '@ckeditor/ckeditor5-ui';
import { Collection, Locale } from '@ckeditor/ckeditor5-utils';

Expand Down Expand Up @@ -58,6 +59,7 @@ window.addToolbarToDropdown = addToolbarToDropdown;
window.createDropdown = createDropdown;
window.createLabeledInputNumber = createLabeledInputNumber;
window.createLabeledInputText = createLabeledInputText;
window.createLabeledInputUrl = createLabeledInputUrl;
window.Collection = Collection;
window.Locale = Locale;

Expand Down
8 changes: 6 additions & 2 deletions docs/_snippets/framework/ui/ui-input.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/

/* globals Locale, LabeledFieldView, createLabeledInputText, createLabeledInputNumber, ToolbarView, document */
/* globals Locale, LabeledFieldView, createLabeledInputText, createLabeledInputNumber, createLabeledInputUrl, ToolbarView, document */

const locale = new Locale();

Expand All @@ -15,7 +15,11 @@ const numberInput = new LabeledFieldView( locale, createLabeledInputNumber );
numberInput.set( { label: 'Number input', value: 'Value of the input' } );
numberInput.render();

const inputs = [ textInput, numberInput ];
const urlInput = new LabeledFieldView( locale, createLabeledInputUrl );
urlInput.set( { label: 'URL input', value: 'https://ckeditor.com/' } );
urlInput.render();

const inputs = [ textInput, numberInput, urlInput ];

const toolbarInputs = new ToolbarView( locale );
inputs.forEach( input => toolbarInputs.items.add( input ) );
Expand Down
28 changes: 25 additions & 3 deletions docs/framework/architecture/ui-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -599,11 +599,16 @@ import loupe from '@ckeditor/ckeditor5-core/theme/icons/loupe.svg';

{@snippet framework/ui/ui-input}

There are also inputs in the CKEditor 5 UI library. There are a few use cases to put inputs inside a main toolbar, but you also can add them to balloon panels.
The CKEditor 5 UI library contains a few input elements. Usually they are used in dropdowns and balloon panels, but you can also use them in a main toolbar.
filipsobol marked this conversation as resolved.
Show resolved Hide resolved

To create them, use the {@link module:ui/labeledfield/labeledfieldview~LabeledFieldView `LabaledFieldView`} class, which takes two parameters:
filipsobol marked this conversation as resolved.
Show resolved Hide resolved

* an instance of the {@link module:ui/labeledfield/labeledfieldview~LabeledFieldView#locale `locale`} class,
* a helper function, depending on the type of field you want to create.

### Text

You can use the {@link module:ui/labeledfield/labeledfieldview~LabeledFieldView `LabaledFieldView`} class to instantiate an input. It takes two parameters: {@link module:ui/labeledfield/labeledfieldview~LabeledFieldView#locale `locale`} and a helper function. Pass the {@link module:ui/labeledfield/utils#createLabeledInputText `createLabeledInputText()`} helper function to create a text input.
To create a text field, pass the {@link module:ui/labeledfield/utils#createLabeledInputText `createLabeledInputText()`} helper function as the second parameter to the {@link module:ui/labeledfield/labeledfieldview~LabeledFieldView `LabaledFieldView`} class.
filipsobol marked this conversation as resolved.
Show resolved Hide resolved

```js
import { createLabeledInputText, LabeledFieldView } from '@ckeditor/ckeditor5-ui';
Expand All @@ -620,7 +625,7 @@ document.getElementById( 'input-text' ).append( textInput.element );

### Number

You can use the {@link module:ui/labeledfield/labeledfieldview~LabeledFieldView `LabaledFieldView`} class to instantiate an input. It takes two parameters: {@link module:ui/labeledfield/labeledfieldview~LabeledFieldView#locale `locale`} and a helper function. Pass the {@link module:ui/labeledfield/utils#createLabeledInputNumber `createLabeledInputNumber()`} helper function to create a number input.
To create a number field, pass the {@link module:ui/labeledfield/utils#createLabeledInputNumber `createLabeledInputNumber()`} helper function as the second parameter to the {@link module:ui/labeledfield/labeledfieldview~LabeledFieldView `LabaledFieldView`} class.
filipsobol marked this conversation as resolved.
Show resolved Hide resolved

```js
import { createLabeledInputNumber, LabeledFieldView } from '@ckeditor/ckeditor5-ui';
Expand All @@ -635,6 +640,23 @@ numberInput.render();
document.getElementById( 'input-number' ).append( numberInput.element );
```

### URL

To create an URL field, pass the {@link module:ui/labeledfield/utils#createLabeledInputUrl `createLabeledInputUrl()`} helper function as the second parameter to the {@link module:ui/labeledfield/labeledfieldview~LabeledFieldView `LabaledFieldView`} class.
filipsobol marked this conversation as resolved.
Show resolved Hide resolved

```js
import { createLabeledInputUrl, LabeledFieldView } from '@ckeditor/ckeditor5-ui';
import { Locale } from '@ckeditor/ckeditor5-utils';

const locale = new Locale();

const urlInput = new LabeledFieldView( locale, createLabeledInputUrl );
urlInput.set( { label: 'URL input', value: 'https://ckeditor.com/' } );
urlInput.render();

document.getElementById( 'input-url' ).append( urlInput.element );
```

### States

{@snippet framework/ui/ui-input-states}
Expand Down
20 changes: 20 additions & 0 deletions packages/ckeditor5-link/src/linkconfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,26 @@ export interface LinkConfig {
*/
defaultProtocol?: string;

/**
* When set to `true`, the form will accept an empty value in the URL field, creating a link with an empty `href` (`<a href="">`).
*
* ```ts
* ClassicEditor
* .create( editorElement, {
* link: {
* allowCreatingEmptyLinks: true
* }
* } )
* .then( ... )
* .catch( ... );
* ```
*
* **NOTE:** This option only adds form validation. If link with empty `href` is loaded into the editor, it will be left as-is.
filipsobol marked this conversation as resolved.
Show resolved Hide resolved
*
* @default false
*/
allowCreatingEmptyLinks?: boolean;

/**
* 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
1 change: 1 addition & 0 deletions packages/ckeditor5-link/src/linkediting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export default class LinkEditing extends Plugin {
super( editor );

editor.config.define( 'link', {
allowCreatingEmptyLinks: false,
addTargetToExternalLinks: false
} );
}
Expand Down
9 changes: 8 additions & 1 deletion packages/ckeditor5-link/src/linkui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,14 +176,21 @@ export default class LinkUI extends Plugin {
const editor = this.editor;
const linkCommand: LinkCommand = editor.commands.get( 'link' )!;
const defaultProtocol = editor.config.get( 'link.defaultProtocol' );
const allowCreatingEmptyLinks = editor.config.get( 'link.allowCreatingEmptyLinks' );

const formView = new ( CssTransitionDisablerMixin( LinkFormView ) )( editor.locale, linkCommand );

formView.urlInputView.fieldView.bind( 'value' ).to( linkCommand, 'value' );

// Form elements should be read-only when corresponding commands are disabled.
formView.urlInputView.bind( 'isEnabled' ).to( linkCommand, 'isEnabled' );
formView.saveButtonView.bind( 'isEnabled' ).to( linkCommand );

// Disable the "save" button if the command is disabled or the input is empty despite being required.
formView.saveButtonView.bind( 'isEnabled' ).to(
linkCommand, 'isEnabled',
formView.urlInputView, 'isEmpty',
( isCommandEnabled, isInputEmpty ) => isCommandEnabled && ( allowCreatingEmptyLinks || !isInputEmpty )
);

// Execute link command after clicking the "Save" button.
this.listenTo( formView, 'submit', () => {
Expand Down
10 changes: 5 additions & 5 deletions packages/ckeditor5-link/src/ui/linkformview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ import {
SwitchButtonView,
View,
ViewCollection,
createLabeledInputText,
createLabeledInputUrl,
submitHandler,
type InputTextView
type InputUrlView
} from 'ckeditor5/src/ui.js';
import {
FocusTracker,
Expand Down Expand Up @@ -53,7 +53,7 @@ export default class LinkFormView extends View {
/**
* The URL input view.
*/
public urlInputView: LabeledFieldView<InputTextView>;
public urlInputView: LabeledFieldView<InputUrlView>;

/**
* The Save button view.
Expand Down Expand Up @@ -207,9 +207,9 @@ export default class LinkFormView extends View {
*
* @returns Labeled field view instance.
*/
private _createUrlInput(): LabeledFieldView<InputTextView> {
private _createUrlInput(): LabeledFieldView<InputUrlView> {
const t = this.locale!.t;
const labeledInput = new LabeledFieldView( this.locale, createLabeledInputText );
const labeledInput = new LabeledFieldView( this.locale, createLabeledInputUrl );

labeledInput.label = t( 'Link URL' );

Expand Down
7 changes: 6 additions & 1 deletion packages/ckeditor5-link/tests/linkediting.js
Original file line number Diff line number Diff line change
Expand Up @@ -633,10 +633,15 @@ describe( 'LinkEditing', () => {
url: 'tel:123456789'
}
];
it( 'link.addTargetToExternalLinks is predefined as false value', () => {

it( 'link.addTargetToExternalLinks has a default value of `false`', () => {
expect( editor.config.get( 'link.addTargetToExternalLinks' ) ).to.be.false;
} );

it( 'link.allowCreatingEmptyLinks has a default value of `false`', () => {
expect( editor.config.get( 'link.allowCreatingEmptyLinks' ) ).to.be.false;
} );

describe( 'for link.addTargetToExternalLinks = false', () => {
let editor, model;

Expand Down
60 changes: 60 additions & 0 deletions packages/ckeditor5-link/tests/linkui.js
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,8 @@ describe( 'LinkUI', () => {
formView = linkUIFeature.formView;
actionsView = linkUIFeature.actionsView;

formView.urlInputView.fieldView.value = 'ckeditor.com';

editor.commands.get( 'link' ).isEnabled = true;
editor.commands.get( 'unlink' ).isEnabled = true;

Expand Down Expand Up @@ -1403,6 +1405,30 @@ describe( 'LinkUI', () => {
} );
};

const createEditorWithEmptyLinks = allowCreatingEmptyLinks => {
return ClassicTestEditor
.create( editorElement, {
plugins: [ LinkEditing, LinkUI, Paragraph, BlockQuote ],
link: { allowCreatingEmptyLinks }
} )
.then( editor => {
const linkUIFeature = editor.plugins.get( LinkUI );

linkUIFeature._createViews();

const formView = linkUIFeature.formView;

formView.render();

editor.model.schema.extend( '$text', {
allowIn: '$root',
allowAttributes: 'linkHref'
} );

return { editor, formView };
} );
};

beforeEach( () => {
// Make sure that forms are lazy initiated.
expect( linkUIFeature.formView ).to.be.null;
Expand All @@ -1427,6 +1453,40 @@ describe( 'LinkUI', () => {
expect( editor.ui.focusTracker.isFocused ).to.be.true;
} );

describe( 'empty links', () => {
it( 'should not allow empty links by default', () => {
const allowCreatingEmptyLinks = editor.config.get( 'link.allowCreatingEmptyLinks' );

expect( allowCreatingEmptyLinks ).to.equal( false );
} );

it( 'should allow enabling empty links', () => {
return createEditorWithEmptyLinks( true ).then( ( { editor } ) => {
const allowCreatingEmptyLinks = editor.config.get( 'link.allowCreatingEmptyLinks' );

expect( allowCreatingEmptyLinks ).to.equal( true );

return editor.destroy();
} );
} );

it( 'should not allow submitting empty form when link is required', () => {
return createEditorWithEmptyLinks( false ).then( ( { editor, formView } ) => {
expect( formView.saveButtonView.isEnabled ).to.be.false;

return editor.destroy();
} );
} );

it( 'should allow submitting empty form when link is not required', () => {
return createEditorWithEmptyLinks( true ).then( ( { editor, formView } ) => {
expect( formView.saveButtonView.isEnabled ).to.be.true;

return editor.destroy();
} );
} );
} );

describe( 'link protocol', () => {
it( 'should use a default link protocol from the `config.link.defaultProtocol` when provided', () => {
return ClassicTestEditor
Expand Down
1 change: 1 addition & 0 deletions packages/ckeditor5-ui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export {

export { default as IconView } from './icon/iconview.js';
export { default as InputView } from './input/inputview.js';
export { default as InputUrlView } from './inputurl/inputurlview.js';
export { default as InputTextView } from './inputtext/inputtextview.js';
export { default as InputNumberView } from './inputnumber/inputnumberview.js';

Expand Down
33 changes: 33 additions & 0 deletions packages/ckeditor5-ui/src/inputurl/inputurlview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/

/**
* @module ui/inputurl/inputurlview
*/

import InputView from '../input/inputview.js';

import type { Locale } from '@ckeditor/ckeditor5-utils';

/**
* The URL input view class.
*/
export default class InputUrlView extends InputView {
/**
* @inheritDoc
*/
constructor( locale?: Locale ) {
super( locale );

this.extendTemplate( {
attributes: {
type: 'url',
class: [
'ck-input-url'
]
}
} );
}
}
Loading