@ckeditor/ckeditor5-table
Version:
Table feature for CKEditor 5.
272 lines (271 loc) • 10.1 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 table/ui/colorinputview
*/
import { View, InputTextView, createDropdown, FocusCycler, ViewCollection, ColorSelectorView } from 'ckeditor5/src/ui.js';
import { FocusTracker, KeystrokeHandler } from 'ckeditor5/src/utils.js';
import '../../theme/colorinput.css';
/**
* The color input view class. It allows the user to type in a color (hex, rgb, etc.)
* or choose it from the configurable color palette with a preview.
*
* @internal
*/
export default class ColorInputView extends View {
/**
* Creates an instance of the color input view.
*
* @param locale The locale instance.
* @param options The input options.
* @param options.colorDefinitions The colors to be displayed in the palette inside the input's dropdown.
* @param options.columns The number of columns in which the colors will be displayed.
* @param options.defaultColorValue If specified, the color input view will replace the "Remove color" button with
* the "Restore default" button. Instead of clearing the input field, the default color value will be set.
*/
constructor(locale, options) {
super(locale);
this.set('value', '');
this.set('isReadOnly', false);
this.set('isFocused', false);
this.set('isEmpty', true);
this.options = options;
this.focusTracker = new FocusTracker();
this._focusables = new ViewCollection();
this.dropdownView = this._createDropdownView();
this.inputView = this._createInputTextView();
this.keystrokes = new KeystrokeHandler();
this._stillTyping = false;
this.focusCycler = new FocusCycler({
focusables: this._focusables,
focusTracker: this.focusTracker,
keystrokeHandler: this.keystrokes,
actions: {
// Navigate items backwards using the <kbd>Shift</kbd> + <kbd>Tab</kbd> keystroke.
focusPrevious: 'shift + tab',
// Navigate items forwards using the <kbd>Tab</kbd> key.
focusNext: 'tab'
}
});
this.setTemplate({
tag: 'div',
attributes: {
class: [
'ck',
'ck-input-color'
]
},
children: [
this.dropdownView,
this.inputView
]
});
this.on('change:value', (evt, name, inputValue) => this._setInputValue(inputValue));
}
/**
* @inheritDoc
*/
render() {
super.render();
[this.inputView, this.dropdownView.buttonView].forEach(view => {
this.focusTracker.add(view.element);
this._focusables.add(view);
});
this.keystrokes.listenTo(this.element);
}
/**
* Focuses the view.
*/
focus(direction) {
if (direction === -1) {
this.focusCycler.focusLast();
}
else {
this.focusCycler.focusFirst();
}
}
/**
* @inheritDoc
*/
destroy() {
super.destroy();
this.focusTracker.destroy();
this.keystrokes.destroy();
}
/**
* Creates and configures the {@link #dropdownView}.
*/
_createDropdownView() {
const locale = this.locale;
const t = locale.t;
const bind = this.bindTemplate;
const colorSelector = this._createColorSelector(locale);
const dropdown = createDropdown(locale);
const colorPreview = new View();
colorPreview.setTemplate({
tag: 'span',
attributes: {
class: [
'ck',
'ck-input-color__button__preview'
],
style: {
backgroundColor: bind.to('value')
}
},
children: [{
tag: 'span',
attributes: {
class: [
'ck',
'ck-input-color__button__preview__no-color-indicator',
bind.if('value', 'ck-hidden', value => value != '')
]
}
}]
});
dropdown.buttonView.extendTemplate({
attributes: {
class: 'ck-input-color__button'
}
});
dropdown.buttonView.children.add(colorPreview);
dropdown.buttonView.label = t('Color picker');
dropdown.buttonView.tooltip = true;
dropdown.panelPosition = locale.uiLanguageDirection === 'rtl' ? 'se' : 'sw';
dropdown.panelView.children.add(colorSelector);
dropdown.bind('isEnabled').to(this, 'isReadOnly', value => !value);
dropdown.on('change:isOpen', (evt, name, isVisible) => {
if (isVisible) {
colorSelector.updateSelectedColors();
colorSelector.showColorGridsFragment();
}
});
return dropdown;
}
/**
* Creates and configures an instance of {@link module:ui/inputtext/inputtextview~InputTextView}.
*
* @returns A configured instance to be set as {@link #inputView}.
*/
_createInputTextView() {
const locale = this.locale;
const inputView = new InputTextView(locale);
inputView.extendTemplate({
on: {
blur: inputView.bindTemplate.to('blur')
}
});
inputView.value = this.value;
inputView.bind('isReadOnly', 'hasError').to(this);
this.bind('isFocused', 'isEmpty').to(inputView);
inputView.on('input', () => {
const inputValue = inputView.element.value;
// Check if the value matches one of our defined colors' label.
const mappedColor = this.options.colorDefinitions.find(def => inputValue === def.label);
this._stillTyping = true;
this.value = mappedColor && mappedColor.color || inputValue;
});
inputView.on('blur', () => {
this._stillTyping = false;
this._setInputValue(inputView.element.value);
});
inputView.delegate('input').to(this);
return inputView;
}
/**
* Creates and configures the panel with "color grid" and "color picker" inside the {@link #dropdownView}.
*/
_createColorSelector(locale) {
const t = locale.t;
const defaultColor = this.options.defaultColorValue || '';
const removeColorButtonLabel = defaultColor ? t('Restore default') : t('Remove color');
const colorSelector = new ColorSelectorView(locale, {
colors: this.options.colorDefinitions,
columns: this.options.columns,
removeButtonLabel: removeColorButtonLabel,
colorPickerLabel: t('Color picker'),
colorPickerViewConfig: this.options.colorPickerConfig === false ? false : {
...this.options.colorPickerConfig,
hideInput: true
}
});
colorSelector.appendUI();
colorSelector.on('execute', (evt, data) => {
if (data.source === 'colorPickerSaveButton') {
this.dropdownView.isOpen = false;
return;
}
this.value = data.value || defaultColor;
// Trigger the listener that actually applies the set value.
this.fire('input');
if (data.source !== 'colorPicker') {
this.dropdownView.isOpen = false;
}
});
/**
* Color is saved before changes in color picker. In case "cancel button" is pressed
* this color will be applied.
*/
let backupColor = this.value;
colorSelector.on('colorPicker:cancel', () => {
/**
* Revert color to previous value before changes in color picker.
*/
this.value = backupColor;
this.fire('input');
this.dropdownView.isOpen = false;
});
colorSelector.colorGridsFragmentView.colorPickerButtonView.on('execute', () => {
/**
* Save color value before changes in color picker.
*/
backupColor = this.value;
});
colorSelector.bind('selectedColor').to(this, 'value');
return colorSelector;
}
/**
* Sets {@link #inputView}'s value property to the color value or color label,
* if there is one and the user is not typing.
*
* Handles cases like:
*
* * Someone picks the color in the grid.
* * The color is set from the plugin level.
*
* @param inputValue Color value to be set.
*/
_setInputValue(inputValue) {
if (!this._stillTyping) {
const normalizedInputValue = normalizeColor(inputValue);
// Check if the value matches one of our defined colors.
const mappedColor = this.options.colorDefinitions.find(def => normalizedInputValue === normalizeColor(def.color));
if (mappedColor) {
this.inputView.value = mappedColor.label;
}
else {
this.inputView.value = inputValue || '';
}
}
}
}
/**
* Normalizes color value, by stripping extensive whitespace.
* For example., transforms:
* * ` rgb( 25 50 0 )` to `rgb(25 50 0)`,
* * "\t rgb( 25 , 50,0 ) " to `rgb(25 50 0)`.
*
* @param colorString The value to be normalized.
*/
function normalizeColor(colorString) {
return colorString
// Remove any whitespace right after `(` or `,`.
.replace(/([(,])\s+/g, '$1')
// Remove any whitespace at the beginning or right before the end, `)`, `,`, or another whitespace.
.replace(/^\s+|\s+(?=[),\s]|$)/g, '')
// Then, replace `,` or whitespace with a single space.
.replace(/,|\s/g, ' ');
}