@ckeditor/ckeditor5-ui
Version:
The UI framework and standard UI library of CKEditor 5.
279 lines (278 loc) • 9.64 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 ui/button/buttonview
*/
import View from '../view.js';
import IconView from '../icon/iconview.js';
import ButtonLabelView from './buttonlabelview.js';
import { env, getEnvKeystrokeText, uid, delay } from '@ckeditor/ckeditor5-utils';
import '../../theme/components/button/button.css';
/**
* The button view class.
*
* ```ts
* const view = new ButtonView();
*
* view.set( {
* label: 'A button',
* keystroke: 'Ctrl+B',
* tooltip: true,
* withText: true
* } );
*
* view.render();
*
* document.body.append( view.element );
* ```
*/
export default class ButtonView extends View {
/**
* Collection of the child views inside of the button {@link #element}.
*/
children;
/**
* Label of the button view. Its text is configurable using the {@link #label label attribute}.
*
* If not configured otherwise in the `constructor()`, by default the label is an instance
* of {@link module:ui/button/buttonlabelview~ButtonLabelView}.
*/
labelView;
/**
* The icon view of the button. Will be added to {@link #children} when the
* {@link #icon icon attribute} is defined.
*/
iconView;
/**
* A view displaying the keystroke of the button next to the {@link #labelView label}.
* Added to {@link #children} when the {@link #withKeystroke `withKeystroke` attribute}
* is defined.
*/
keystrokeView;
/**
* Delayed focus function for focus handling in Safari.
*/
_focusDelayed = null;
/**
* Creates an instance of the button view class.
*
* @param locale The {@link module:core/editor/editor~Editor#locale} instance.
* @param labelView The instance of the button's label. If not provided, an instance of
* {@link module:ui/button/buttonlabelview~ButtonLabelView} is used.
*/
constructor(locale, labelView = new ButtonLabelView()) {
super(locale);
const bind = this.bindTemplate;
const ariaLabelUid = uid();
// Implement the Button interface.
this.set('_ariaPressed', false);
this.set('_ariaChecked', false);
this.set('ariaLabel', undefined);
this.set('ariaLabelledBy', `ck-editor__aria-label_${ariaLabelUid}`);
this.set('class', undefined);
this.set('labelStyle', undefined);
this.set('icon', undefined);
this.set('isEnabled', true);
this.set('isOn', false);
this.set('isVisible', true);
this.set('isToggleable', false);
this.set('keystroke', undefined);
this.set('label', undefined);
this.set('role', undefined);
this.set('tabindex', -1);
this.set('tooltip', false);
this.set('tooltipPosition', 's');
this.set('type', 'button');
this.set('withText', false);
this.set('withKeystroke', false);
this.children = this.createCollection();
this.labelView = this._setupLabelView(labelView);
this.iconView = new IconView();
this.iconView.extendTemplate({
attributes: {
class: 'ck-button__icon'
}
});
this.iconView.bind('content').to(this, 'icon');
this.keystrokeView = this._createKeystrokeView();
this.bind('_tooltipString').to(this, 'tooltip', this, 'label', this, 'keystroke', this._getTooltipString.bind(this));
const template = {
tag: 'button',
attributes: {
class: [
'ck',
'ck-button',
bind.to('class'),
bind.if('isEnabled', 'ck-disabled', value => !value),
bind.if('isVisible', 'ck-hidden', value => !value),
bind.to('isOn', value => value ? 'ck-on' : 'ck-off'),
bind.if('withText', 'ck-button_with-text'),
bind.if('withKeystroke', 'ck-button_with-keystroke')
],
role: bind.to('role'),
type: bind.to('type', value => value ? value : 'button'),
tabindex: bind.to('tabindex'),
'aria-checked': bind.to('_ariaChecked'),
'aria-pressed': bind.to('_ariaPressed'),
'aria-label': bind.to('ariaLabel'),
'aria-labelledby': bind.to('ariaLabelledBy'),
'aria-disabled': bind.if('isEnabled', true, value => !value),
'data-cke-tooltip-text': bind.to('_tooltipString'),
'data-cke-tooltip-position': bind.to('tooltipPosition')
},
children: this.children,
on: {
click: bind.to(evt => {
// We can't make the button disabled using the disabled attribute, because it won't be focusable.
// Though, shouldn't this condition be moved to the button controller?
if (this.isEnabled) {
this.fire('execute');
}
else {
// Prevent the default when button is disabled, to block e.g.
// automatic form submitting. See ckeditor/ckeditor5-link#74.
evt.preventDefault();
}
})
}
};
this.bind('_ariaPressed').to(this, 'isOn', this, 'isToggleable', this, 'role', (isOn, isToggleable, role) => {
if (!isToggleable || isCheckableRole(role)) {
return false;
}
return String(!!isOn);
});
this.bind('_ariaChecked').to(this, 'isOn', this, 'isToggleable', this, 'role', (isOn, isToggleable, role) => {
if (!isToggleable || !isCheckableRole(role)) {
return false;
}
return String(!!isOn);
});
// On Safari we have to force the focus on a button on click as it's the only browser
// that doesn't do that automatically. See #12115.
if (env.isSafari) {
if (!this._focusDelayed) {
this._focusDelayed = delay(() => this.focus(), 0);
}
template.on.mousedown = bind.to(() => {
this._focusDelayed();
});
template.on.mouseup = bind.to(() => {
this._focusDelayed.cancel();
});
}
this.setTemplate(template);
}
/**
* @inheritDoc
*/
render() {
super.render();
if (this.icon) {
this.children.add(this.iconView);
}
this.on('change:icon', (evt, prop, newIcon, oldIcon) => {
if (newIcon && !oldIcon) {
this.children.add(this.iconView, 0);
}
else if (!newIcon && oldIcon) {
this.children.remove(this.iconView);
}
});
this.children.add(this.labelView);
if (this.withKeystroke && this.keystroke) {
this.children.add(this.keystrokeView);
}
}
/**
* Focuses the {@link #element} of the button.
*/
focus() {
this.element.focus();
}
/**
* @inheritDoc
*/
destroy() {
if (this._focusDelayed) {
this._focusDelayed.cancel();
}
super.destroy();
}
/**
* Binds the label view instance it with button attributes.
*/
_setupLabelView(labelView) {
labelView.bind('text', 'style', 'id').to(this, 'label', 'labelStyle', 'ariaLabelledBy');
return labelView;
}
/**
* Creates a view that displays a keystroke next to a {@link #labelView label }
* and binds it with button attributes.
*/
_createKeystrokeView() {
const keystrokeView = new View();
keystrokeView.setTemplate({
tag: 'span',
attributes: {
class: [
'ck',
'ck-button__keystroke'
]
},
children: [
{
text: this.bindTemplate.to('keystroke', text => getEnvKeystrokeText(text))
}
]
});
return keystrokeView;
}
/**
* Gets the text for the tooltip from the combination of
* {@link #tooltip}, {@link #label} and {@link #keystroke} attributes.
*
* @see #tooltip
* @see #_tooltipString
* @param tooltip Button tooltip.
* @param label Button label.
* @param keystroke Button keystroke.
*/
_getTooltipString(tooltip, label, keystroke) {
if (tooltip) {
if (typeof tooltip == 'string') {
return tooltip;
}
else {
if (keystroke) {
keystroke = getEnvKeystrokeText(keystroke);
}
if (tooltip instanceof Function) {
return tooltip(label, keystroke);
}
else {
return `${label}${keystroke ? ` (${keystroke})` : ''}`;
}
}
}
return '';
}
}
/**
* Checks if `aria-checkbox` can be used with specified role.
*/
function isCheckableRole(role) {
switch (role) {
case 'radio':
case 'checkbox':
case 'option':
case 'switch':
case 'menuitemcheckbox':
case 'menuitemradio':
return true;
default:
return false;
}
}