UNPKG

13.6 kBJavaScriptView Raw
1/**
2 * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
3 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4 */
5/**
6 * @module table/tablecellproperties/tablecellpropertiesui
7 */
8import { Plugin } from 'ckeditor5/src/core';
9import { ButtonView, clickOutsideHandler, ContextualBalloon, getLocalizedColorOptions, normalizeColorOptions } from 'ckeditor5/src/ui';
10import TableCellPropertiesView from './ui/tablecellpropertiesview';
11import { colorFieldValidator, getLocalizedColorErrorText, getLocalizedLengthErrorText, defaultColors, lengthFieldValidator, lineWidthFieldValidator } from '../utils/ui/table-properties';
12import { debounce } from 'lodash-es';
13import { getTableWidgetAncestor } from '../utils/ui/widget';
14import { getBalloonCellPositionData, repositionContextualBalloon } from '../utils/ui/contextualballoon';
15import tableCellProperties from './../../theme/icons/table-cell-properties.svg';
16import { getNormalizedDefaultProperties } from '../utils/table-properties';
17const ERROR_TEXT_TIMEOUT = 500;
18// Map of view properties and related commands.
19const propertyToCommandMap = {
20 borderStyle: 'tableCellBorderStyle',
21 borderColor: 'tableCellBorderColor',
22 borderWidth: 'tableCellBorderWidth',
23 height: 'tableCellHeight',
24 width: 'tableCellWidth',
25 padding: 'tableCellPadding',
26 backgroundColor: 'tableCellBackgroundColor',
27 horizontalAlignment: 'tableCellHorizontalAlignment',
28 verticalAlignment: 'tableCellVerticalAlignment'
29};
30/**
31 * The table cell properties UI plugin. It introduces the `'tableCellProperties'` button
32 * that opens a form allowing to specify the visual styling of a table cell.
33 *
34 * It uses the {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon contextual balloon plugin}.
35 */
36export default class TableCellPropertiesUI extends Plugin {
37 /**
38 * @inheritDoc
39 */
40 static get requires() {
41 return [ContextualBalloon];
42 }
43 /**
44 * @inheritDoc
45 */
46 static get pluginName() {
47 return 'TableCellPropertiesUI';
48 }
49 /**
50 * @inheritDoc
51 */
52 constructor(editor) {
53 super(editor);
54 editor.config.define('table.tableCellProperties', {
55 borderColors: defaultColors,
56 backgroundColors: defaultColors
57 });
58 }
59 /**
60 * @inheritDoc
61 */
62 init() {
63 const editor = this.editor;
64 const t = editor.t;
65 this._defaultTableCellProperties = getNormalizedDefaultProperties(editor.config.get('table.tableCellProperties.defaultProperties'), {
66 includeVerticalAlignmentProperty: true,
67 includeHorizontalAlignmentProperty: true,
68 includePaddingProperty: true,
69 isRightToLeftContent: editor.locale.contentLanguageDirection === 'rtl'
70 });
71 this._balloon = editor.plugins.get(ContextualBalloon);
72 this.view = null;
73 this._isReady = false;
74 editor.ui.componentFactory.add('tableCellProperties', locale => {
75 const view = new ButtonView(locale);
76 view.set({
77 label: t('Cell properties'),
78 icon: tableCellProperties,
79 tooltip: true
80 });
81 this.listenTo(view, 'execute', () => this._showView());
82 const commands = Object.values(propertyToCommandMap)
83 .map(commandName => editor.commands.get(commandName));
84 view.bind('isEnabled').toMany(commands, 'isEnabled', (...areEnabled) => (areEnabled.some(isCommandEnabled => isCommandEnabled)));
85 return view;
86 });
87 }
88 /**
89 * @inheritDoc
90 */
91 destroy() {
92 super.destroy();
93 // Destroy created UI components as they are not automatically destroyed.
94 // See https://github.com/ckeditor/ckeditor5/issues/1341.
95 if (this.view) {
96 this.view.destroy();
97 }
98 }
99 /**
100 * Creates the {@link module:table/tablecellproperties/ui/tablecellpropertiesview~TableCellPropertiesView} instance.
101 *
102 * @returns The cell properties form view instance.
103 */
104 _createPropertiesView() {
105 const editor = this.editor;
106 const config = editor.config.get('table.tableCellProperties');
107 const borderColorsConfig = normalizeColorOptions(config.borderColors);
108 const localizedBorderColors = getLocalizedColorOptions(editor.locale, borderColorsConfig);
109 const backgroundColorsConfig = normalizeColorOptions(config.backgroundColors);
110 const localizedBackgroundColors = getLocalizedColorOptions(editor.locale, backgroundColorsConfig);
111 const view = new TableCellPropertiesView(editor.locale, {
112 borderColors: localizedBorderColors,
113 backgroundColors: localizedBackgroundColors,
114 defaultTableCellProperties: this._defaultTableCellProperties
115 });
116 const t = editor.t;
117 // Render the view so its #element is available for the clickOutsideHandler.
118 view.render();
119 this.listenTo(view, 'submit', () => {
120 this._hideView();
121 });
122 this.listenTo(view, 'cancel', () => {
123 // https://github.com/ckeditor/ckeditor5/issues/6180
124 if (this._undoStepBatch.operations.length) {
125 editor.execute('undo', this._undoStepBatch);
126 }
127 this._hideView();
128 });
129 // Close the balloon on Esc key press.
130 view.keystrokes.set('Esc', (data, cancel) => {
131 this._hideView();
132 cancel();
133 });
134 // Close on click outside of balloon panel element.
135 clickOutsideHandler({
136 emitter: view,
137 activator: () => this._isViewInBalloon,
138 contextElements: [this._balloon.view.element],
139 callback: () => this._hideView()
140 });
141 const colorErrorText = getLocalizedColorErrorText(t);
142 const lengthErrorText = getLocalizedLengthErrorText(t);
143 // Create the "UI -> editor data" binding.
144 // These listeners update the editor data (via table commands) when any observable
145 // property of the view has changed. They also validate the value and display errors in the UI
146 // when necessary. This makes the view live, which means the changes are
147 // visible in the editing as soon as the user types or changes fields' values.
148 view.on('change:borderStyle', this._getPropertyChangeCallback('tableCellBorderStyle'));
149 view.on('change:borderColor', this._getValidatedPropertyChangeCallback({
150 viewField: view.borderColorInput,
151 commandName: 'tableCellBorderColor',
152 errorText: colorErrorText,
153 validator: colorFieldValidator
154 }));
155 view.on('change:borderWidth', this._getValidatedPropertyChangeCallback({
156 viewField: view.borderWidthInput,
157 commandName: 'tableCellBorderWidth',
158 errorText: lengthErrorText,
159 validator: lineWidthFieldValidator
160 }));
161 view.on('change:padding', this._getValidatedPropertyChangeCallback({
162 viewField: view.paddingInput,
163 commandName: 'tableCellPadding',
164 errorText: lengthErrorText,
165 validator: lengthFieldValidator
166 }));
167 view.on('change:width', this._getValidatedPropertyChangeCallback({
168 viewField: view.widthInput,
169 commandName: 'tableCellWidth',
170 errorText: lengthErrorText,
171 validator: lengthFieldValidator
172 }));
173 view.on('change:height', this._getValidatedPropertyChangeCallback({
174 viewField: view.heightInput,
175 commandName: 'tableCellHeight',
176 errorText: lengthErrorText,
177 validator: lengthFieldValidator
178 }));
179 view.on('change:backgroundColor', this._getValidatedPropertyChangeCallback({
180 viewField: view.backgroundInput,
181 commandName: 'tableCellBackgroundColor',
182 errorText: colorErrorText,
183 validator: colorFieldValidator
184 }));
185 view.on('change:horizontalAlignment', this._getPropertyChangeCallback('tableCellHorizontalAlignment'));
186 view.on('change:verticalAlignment', this._getPropertyChangeCallback('tableCellVerticalAlignment'));
187 return view;
188 }
189 /**
190 * In this method the "editor data -> UI" binding is happening.
191 *
192 * When executed, this method obtains selected cell property values from various table commands
193 * and passes them to the {@link #view}.
194 *
195 * This way, the UI stays up–to–date with the editor data.
196 */
197 _fillViewFormFromCommandValues() {
198 const commands = this.editor.commands;
199 const borderStyleCommand = commands.get('tableCellBorderStyle');
200 Object.entries(propertyToCommandMap)
201 .map(([property, commandName]) => {
202 const defaultValue = this._defaultTableCellProperties[property] || '';
203 return [
204 property,
205 commands.get(commandName).value || defaultValue
206 ];
207 })
208 .forEach(([property, value]) => {
209 // Do not set the `border-color` and `border-width` fields if `border-style:none`.
210 if ((property === 'borderColor' || property === 'borderWidth') && borderStyleCommand.value === 'none') {
211 return;
212 }
213 this.view.set(property, value);
214 });
215 this._isReady = true;
216 }
217 /**
218 * Shows the {@link #view} in the {@link #_balloon}.
219 *
220 * **Note**: Each time a view is shown, a new {@link #_undoStepBatch} is created. It contains
221 * all changes made to the document when the view is visible, allowing a single undo step
222 * for all of them.
223 */
224 _showView() {
225 const editor = this.editor;
226 if (!this.view) {
227 this.view = this._createPropertiesView();
228 }
229 this.listenTo(editor.ui, 'update', () => {
230 this._updateView();
231 });
232 // Update the view with the model values.
233 this._fillViewFormFromCommandValues();
234 this._balloon.add({
235 view: this.view,
236 position: getBalloonCellPositionData(editor)
237 });
238 // Create a new batch. Clicking "Cancel" will undo this batch.
239 this._undoStepBatch = editor.model.createBatch();
240 // Basic a11y.
241 this.view.focus();
242 }
243 /**
244 * Removes the {@link #view} from the {@link #_balloon}.
245 */
246 _hideView() {
247 const editor = this.editor;
248 this.stopListening(editor.ui, 'update');
249 this._isReady = false;
250 // Blur any input element before removing it from DOM to prevent issues in some browsers.
251 // See https://github.com/ckeditor/ckeditor5/issues/1501.
252 this.view.saveButtonView.focus();
253 this._balloon.remove(this.view);
254 // Make sure the focus is not lost in the process by putting it directly
255 // into the editing view.
256 this.editor.editing.view.focus();
257 }
258 /**
259 * Repositions the {@link #_balloon} or hides the {@link #view} if a table cell is no longer selected.
260 */
261 _updateView() {
262 const editor = this.editor;
263 const viewDocument = editor.editing.view.document;
264 if (!getTableWidgetAncestor(viewDocument.selection)) {
265 this._hideView();
266 }
267 else if (this._isViewVisible) {
268 repositionContextualBalloon(editor, 'cell');
269 }
270 }
271 /**
272 * Returns `true` when the {@link #view} is visible in the {@link #_balloon}.
273 */
274 get _isViewVisible() {
275 return !!this.view && this._balloon.visibleView === this.view;
276 }
277 /**
278 * Returns `true` when the {@link #view} is in the {@link #_balloon}.
279 */
280 get _isViewInBalloon() {
281 return !!this.view && this._balloon.hasView(this.view);
282 }
283 /**
284 * Creates a callback that when executed upon the {@link #view view's} property change
285 * executes a related editor command with the new property value.
286 *
287 * @param defaultValue The default value of the command.
288 */
289 _getPropertyChangeCallback(commandName) {
290 return (evt, propertyName, newValue) => {
291 if (!this._isReady) {
292 return;
293 }
294 this.editor.execute(commandName, {
295 value: newValue,
296 batch: this._undoStepBatch
297 });
298 };
299 }
300 /**
301 * Creates a callback that when executed upon the {@link #view view's} property change:
302 * * Executes a related editor command with the new property value if the value is valid,
303 * * Or sets the error text next to the invalid field, if the value did not pass the validation.
304 */
305 _getValidatedPropertyChangeCallback(options) {
306 const { commandName, viewField, validator, errorText } = options;
307 const setErrorTextDebounced = debounce(() => {
308 viewField.errorText = errorText;
309 }, ERROR_TEXT_TIMEOUT);
310 return (evt, propertyName, newValue) => {
311 setErrorTextDebounced.cancel();
312 // Do not execute the command on initial call (opening the table properties view).
313 if (!this._isReady) {
314 return;
315 }
316 if (validator(newValue)) {
317 this.editor.execute(commandName, {
318 value: newValue,
319 batch: this._undoStepBatch
320 });
321 viewField.errorText = null;
322 }
323 else {
324 setErrorTextDebounced();
325 }
326 };
327 }
328}