@ckeditor/ckeditor5-link
Version:
Link feature for CKEditor 5.
303 lines (302 loc) • 9.73 kB
JavaScript
/**
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module link/ui/linkformview
*/
import { ButtonView, ListView, ListItemView, FocusCycler, LabeledFieldView, FormHeaderView, FormRowView, View, ViewCollection, createLabeledInputText, submitHandler } from 'ckeditor5/src/ui.js';
import { FocusTracker, KeystrokeHandler } from 'ckeditor5/src/utils.js';
import { IconPreviousArrow } from 'ckeditor5/src/icons.js';
// See: #8833.
// eslint-disable-next-line ckeditor5-rules/ckeditor-imports
import '@ckeditor/ckeditor5-ui/theme/components/responsive-form/responsiveform.css';
// eslint-disable-next-line ckeditor5-rules/ckeditor-imports
import '@ckeditor/ckeditor5-ui/theme/components/form/form.css';
import '../../theme/linkform.css';
/**
* The link form view.
*/
export default class LinkFormView extends View {
/**
* Tracks information about DOM focus in the form.
*/
focusTracker = new FocusTracker();
/**
* An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
*/
keystrokes = new KeystrokeHandler();
/**
* The Back button view displayed in the header.
*/
backButtonView;
/**
* The Save button view.
*/
saveButtonView;
/**
* The "Displayed text" input view.
*/
displayedTextInputView;
/**
* The URL input view.
*/
urlInputView;
/**
* A collection of child views.
*/
children;
/**
* A collection of child views in the providers list.
*/
providersListChildren;
/**
* An array of form validators used by {@link #isValid}.
*/
_validators;
/**
* A collection of views that can be focused in the form.
*/
_focusables = new ViewCollection();
/**
* Helps cycling over {@link #_focusables} in the form.
*/
_focusCycler;
/**
* Creates an instance of the {@link module:link/ui/linkformview~LinkFormView} class.
*
* Also see {@link #render}.
*
* @param locale The localization services instance.
* @param validators Form validators used by {@link #isValid}.
*/
constructor(locale, validators) {
super(locale);
this._validators = validators;
// Create buttons.
this.backButtonView = this._createBackButton();
this.saveButtonView = this._createSaveButton();
// Create input fields.
this.displayedTextInputView = this._createDisplayedTextInput();
this.urlInputView = this._createUrlInput();
this.providersListChildren = this.createCollection();
this.children = this.createCollection([
this._createHeaderView()
]);
this._createFormChildren();
// Add providers list view to the children when the first item is added to the list.
// This is to avoid adding the list view when the form is empty.
this.listenTo(this.providersListChildren, 'add', () => {
this.stopListening(this.providersListChildren, 'add');
this.children.add(this._createProvidersListView());
});
this._focusCycler = new FocusCycler({
focusables: this._focusables,
focusTracker: this.focusTracker,
keystrokeHandler: this.keystrokes,
actions: {
// Navigate form fields backwards using the Shift + Tab keystroke.
focusPrevious: 'shift + tab',
// Navigate form fields forwards using the Tab key.
focusNext: 'tab'
}
});
this.setTemplate({
tag: 'form',
attributes: {
class: [
'ck',
'ck-form',
'ck-link-form',
'ck-responsive-form'
],
// https://github.com/ckeditor/ckeditor5-link/issues/90
tabindex: '-1'
},
children: this.children
});
}
/**
* @inheritDoc
*/
render() {
super.render();
submitHandler({
view: this
});
const childViews = [
this.urlInputView,
this.saveButtonView,
...this.providersListChildren,
this.backButtonView,
this.displayedTextInputView
];
childViews.forEach(v => {
// Register the view as focusable.
this._focusables.add(v);
// Register the view in the focus tracker.
this.focusTracker.add(v.element);
});
// Start listening for the keystrokes coming from #element.
this.keystrokes.listenTo(this.element);
}
/**
* @inheritDoc
*/
destroy() {
super.destroy();
this.focusTracker.destroy();
this.keystrokes.destroy();
}
/**
* Focuses the fist {@link #_focusables} in the form.
*/
focus() {
this._focusCycler.focusFirst();
}
/**
* Validates the form and returns `false` when some fields are invalid.
*/
isValid() {
this.resetFormStatus();
for (const validator of this._validators) {
const errorText = validator(this);
// One error per field is enough.
if (errorText) {
// Apply updated error.
this.urlInputView.errorText = errorText;
return false;
}
}
return true;
}
/**
* Cleans up the supplementary error and information text of the {@link #urlInputView}
* bringing them back to the state when the form has been displayed for the first time.
*
* See {@link #isValid}.
*/
resetFormStatus() {
this.urlInputView.errorText = null;
}
/**
* Creates a back button view that cancels the form.
*/
_createBackButton() {
const t = this.locale.t;
const backButton = new ButtonView(this.locale);
backButton.set({
class: 'ck-button-back',
label: t('Back'),
icon: IconPreviousArrow,
tooltip: true
});
backButton.delegate('execute').to(this, 'cancel');
return backButton;
}
/**
* Creates a save button view that inserts the link.
*/
_createSaveButton() {
const t = this.locale.t;
const saveButton = new ButtonView(this.locale);
saveButton.set({
label: t('Insert'),
tooltip: false,
withText: true,
type: 'submit',
class: 'ck-button-action ck-button-bold'
});
return saveButton;
}
/**
* Creates a header view for the form.
*/
_createHeaderView() {
const t = this.locale.t;
const header = new FormHeaderView(this.locale, {
label: t('Link')
});
header.children.add(this.backButtonView, 0);
return header;
}
/**
* Creates a view for the providers list.
*/
_createProvidersListView() {
const providersListView = new ListView(this.locale);
providersListView.extendTemplate({
attributes: {
class: [
'ck-link-form__providers-list'
]
}
});
providersListView.items.bindTo(this.providersListChildren).using(def => {
const listItemView = new ListItemView(this.locale);
listItemView.children.add(def);
return listItemView;
});
return providersListView;
}
/**
* Creates a labeled input view for the "Displayed text" field.
*/
_createDisplayedTextInput() {
const t = this.locale.t;
const labeledInput = new LabeledFieldView(this.locale, createLabeledInputText);
labeledInput.label = t('Displayed text');
labeledInput.class = 'ck-labeled-field-view_full-width';
return labeledInput;
}
/**
* Creates a labeled input view for the URL field.
*
* @returns Labeled field view instance.
*/
_createUrlInput() {
const t = this.locale.t;
const labeledInput = new LabeledFieldView(this.locale, createLabeledInputText);
labeledInput.fieldView.inputMode = 'url';
labeledInput.label = t('Link URL');
labeledInput.class = 'ck-labeled-field-view_full-width';
return labeledInput;
}
/**
* Populates the {@link #children} collection of the form.
*/
_createFormChildren() {
this.children.add(new FormRowView(this.locale, {
children: [
this.displayedTextInputView
],
class: [
'ck-form__row_large-top-padding'
]
}));
this.children.add(new FormRowView(this.locale, {
children: [
this.urlInputView,
this.saveButtonView
],
class: [
'ck-form__row_with-submit',
'ck-form__row_large-top-padding',
'ck-form__row_large-bottom-padding'
]
}));
}
/**
* The native DOM `value` of the {@link #urlInputView} element.
*
* **Note**: Do not confuse it with the {@link module:ui/inputtext/inputtextview~InputTextView#value}
* which works one way only and may not represent the actual state of the component in the DOM.
*/
get url() {
const { element } = this.urlInputView.fieldView;
if (!element) {
return null;
}
return element.value.trim();
}
}