UNPKG

21 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/ui/tablecellpropertiesview
7 */
8import { addListToDropdown, ButtonView, createLabeledDropdown, createLabeledInputText, FocusCycler, FormHeaderView, LabeledFieldView, LabelView, submitHandler, ToolbarView, View, ViewCollection } from 'ckeditor5/src/ui';
9import { KeystrokeHandler, FocusTracker } from 'ckeditor5/src/utils';
10import { icons } from 'ckeditor5/src/core';
11import { fillToolbar, getBorderStyleDefinitions, getBorderStyleLabels, getLabeledColorInputCreator } from '../../utils/ui/table-properties';
12import FormRowView from '../../ui/formrowview';
13import '../../../theme/form.css';
14import '../../../theme/tableform.css';
15import '../../../theme/tablecellproperties.css';
16const ALIGNMENT_ICONS = {
17 left: icons.alignLeft,
18 center: icons.alignCenter,
19 right: icons.alignRight,
20 justify: icons.alignJustify,
21 top: icons.alignTop,
22 middle: icons.alignMiddle,
23 bottom: icons.alignBottom
24};
25/**
26 * The class representing a table cell properties form, allowing users to customize
27 * certain style aspects of a table cell, for instance, border, padding, text alignment, etc..
28 */
29export default class TableCellPropertiesView extends View {
30 /**
31 * @param locale The {@link module:core/editor/editor~Editor#locale} instance.
32 * @param options Additional configuration of the view.
33 * @param options.borderColors A configuration of the border color palette used by the
34 * {@link module:table/tablecellproperties/ui/tablecellpropertiesview~TableCellPropertiesView#borderColorInput}.
35 * @param options.backgroundColors A configuration of the background color palette used by the
36 * {@link module:table/tablecellproperties/ui/tablecellpropertiesview~TableCellPropertiesView#backgroundInput}.
37 * @param options.defaultTableCellProperties The default table cell properties.
38 */
39 constructor(locale, options) {
40 super(locale);
41 this.set({
42 borderStyle: '',
43 borderWidth: '',
44 borderColor: '',
45 padding: '',
46 backgroundColor: '',
47 width: '',
48 height: '',
49 horizontalAlignment: '',
50 verticalAlignment: ''
51 });
52 this.options = options;
53 const { borderStyleDropdown, borderWidthInput, borderColorInput, borderRowLabel } = this._createBorderFields();
54 const { backgroundRowLabel, backgroundInput } = this._createBackgroundFields();
55 const { widthInput, operatorLabel, heightInput, dimensionsLabel } = this._createDimensionFields();
56 const { horizontalAlignmentToolbar, verticalAlignmentToolbar, alignmentLabel } = this._createAlignmentFields();
57 this.focusTracker = new FocusTracker();
58 this.keystrokes = new KeystrokeHandler();
59 this.children = this.createCollection();
60 this.borderStyleDropdown = borderStyleDropdown;
61 this.borderWidthInput = borderWidthInput;
62 this.borderColorInput = borderColorInput;
63 this.backgroundInput = backgroundInput;
64 this.paddingInput = this._createPaddingField();
65 this.widthInput = widthInput;
66 this.heightInput = heightInput;
67 this.horizontalAlignmentToolbar = horizontalAlignmentToolbar;
68 this.verticalAlignmentToolbar = verticalAlignmentToolbar;
69 // Defer creating to make sure other fields are present and the Save button can
70 // bind its #isEnabled to their error messages so there's no way to save unless all
71 // fields are valid.
72 const { saveButtonView, cancelButtonView } = this._createActionButtons();
73 this.saveButtonView = saveButtonView;
74 this.cancelButtonView = cancelButtonView;
75 this._focusables = new ViewCollection();
76 this._focusCycler = new FocusCycler({
77 focusables: this._focusables,
78 focusTracker: this.focusTracker,
79 keystrokeHandler: this.keystrokes,
80 actions: {
81 // Navigate form fields backwards using the Shift + Tab keystroke.
82 focusPrevious: 'shift + tab',
83 // Navigate form fields forwards using the Tab key.
84 focusNext: 'tab'
85 }
86 });
87 // Form header.
88 this.children.add(new FormHeaderView(locale, {
89 label: this.t('Cell properties')
90 }));
91 // Border row.
92 this.children.add(new FormRowView(locale, {
93 labelView: borderRowLabel,
94 children: [
95 borderRowLabel,
96 borderStyleDropdown,
97 borderColorInput,
98 borderWidthInput
99 ],
100 class: 'ck-table-form__border-row'
101 }));
102 // Background.
103 this.children.add(new FormRowView(locale, {
104 labelView: backgroundRowLabel,
105 children: [
106 backgroundRowLabel,
107 backgroundInput
108 ],
109 class: 'ck-table-form__background-row'
110 }));
111 // Dimensions row and padding.
112 this.children.add(new FormRowView(locale, {
113 children: [
114 // Dimensions row.
115 new FormRowView(locale, {
116 labelView: dimensionsLabel,
117 children: [
118 dimensionsLabel,
119 widthInput,
120 operatorLabel,
121 heightInput
122 ],
123 class: 'ck-table-form__dimensions-row'
124 }),
125 // Padding row.
126 new FormRowView(locale, {
127 children: [
128 this.paddingInput
129 ],
130 class: 'ck-table-cell-properties-form__padding-row'
131 })
132 ]
133 }));
134 // Text alignment row.
135 this.children.add(new FormRowView(locale, {
136 labelView: alignmentLabel,
137 children: [
138 alignmentLabel,
139 horizontalAlignmentToolbar,
140 verticalAlignmentToolbar
141 ],
142 class: 'ck-table-cell-properties-form__alignment-row'
143 }));
144 // Action row.
145 this.children.add(new FormRowView(locale, {
146 children: [
147 this.saveButtonView,
148 this.cancelButtonView
149 ],
150 class: 'ck-table-form__action-row'
151 }));
152 this.setTemplate({
153 tag: 'form',
154 attributes: {
155 class: [
156 'ck',
157 'ck-form',
158 'ck-table-form',
159 'ck-table-cell-properties-form'
160 ],
161 // https://github.com/ckeditor/ckeditor5-link/issues/90
162 tabindex: '-1'
163 },
164 children: this.children
165 });
166 }
167 /**
168 * @inheritDoc
169 */
170 render() {
171 super.render();
172 // Enable the "submit" event for this view. It can be triggered by the #saveButtonView
173 // which is of the "submit" DOM "type".
174 submitHandler({
175 view: this
176 });
177 [
178 this.borderStyleDropdown,
179 this.borderColorInput,
180 this.borderColorInput.fieldView.dropdownView.buttonView,
181 this.borderWidthInput,
182 this.backgroundInput,
183 this.backgroundInput.fieldView.dropdownView.buttonView,
184 this.widthInput,
185 this.heightInput,
186 this.paddingInput,
187 this.horizontalAlignmentToolbar,
188 this.verticalAlignmentToolbar,
189 this.saveButtonView,
190 this.cancelButtonView
191 ].forEach(view => {
192 // Register the view as focusable.
193 this._focusables.add(view);
194 // Register the view in the focus tracker.
195 this.focusTracker.add(view.element);
196 });
197 // Mainly for closing using "Esc" and navigation using "Tab".
198 this.keystrokes.listenTo(this.element);
199 }
200 /**
201 * @inheritDoc
202 */
203 destroy() {
204 super.destroy();
205 this.focusTracker.destroy();
206 this.keystrokes.destroy();
207 }
208 /**
209 * Focuses the fist focusable field in the form.
210 */
211 focus() {
212 this._focusCycler.focusFirst();
213 }
214 /**
215 * Creates the following form fields:
216 *
217 * * {@link #borderStyleDropdown},
218 * * {@link #borderWidthInput},
219 * * {@link #borderColorInput}.
220 */
221 _createBorderFields() {
222 const defaultTableCellProperties = this.options.defaultTableCellProperties;
223 const defaultBorder = {
224 style: defaultTableCellProperties.borderStyle,
225 width: defaultTableCellProperties.borderWidth,
226 color: defaultTableCellProperties.borderColor
227 };
228 const colorInputCreator = getLabeledColorInputCreator({
229 colorConfig: this.options.borderColors,
230 columns: 5,
231 defaultColorValue: defaultBorder.color
232 });
233 const locale = this.locale;
234 const t = this.t;
235 const accessibleLabel = t('Style');
236 // -- Group label ---------------------------------------------
237 const borderRowLabel = new LabelView(locale);
238 borderRowLabel.text = t('Border');
239 // -- Style ---------------------------------------------------
240 const styleLabels = getBorderStyleLabels(t);
241 const borderStyleDropdown = new LabeledFieldView(locale, createLabeledDropdown);
242 borderStyleDropdown.set({
243 label: accessibleLabel,
244 class: 'ck-table-form__border-style'
245 });
246 borderStyleDropdown.fieldView.buttonView.set({
247 ariaLabel: accessibleLabel,
248 ariaLabelledBy: undefined,
249 isOn: false,
250 withText: true,
251 tooltip: accessibleLabel
252 });
253 borderStyleDropdown.fieldView.buttonView.bind('label').to(this, 'borderStyle', value => {
254 return styleLabels[value ? value : 'none'];
255 });
256 borderStyleDropdown.fieldView.on('execute', evt => {
257 this.borderStyle = evt.source._borderStyleValue;
258 });
259 borderStyleDropdown.bind('isEmpty').to(this, 'borderStyle', value => !value);
260 addListToDropdown(borderStyleDropdown.fieldView, getBorderStyleDefinitions(this, defaultBorder.style), {
261 role: 'menu',
262 ariaLabel: accessibleLabel
263 });
264 // -- Width ---------------------------------------------------
265 const borderWidthInput = new LabeledFieldView(locale, createLabeledInputText);
266 borderWidthInput.set({
267 label: t('Width'),
268 class: 'ck-table-form__border-width'
269 });
270 borderWidthInput.fieldView.bind('value').to(this, 'borderWidth');
271 borderWidthInput.bind('isEnabled').to(this, 'borderStyle', isBorderStyleSet);
272 borderWidthInput.fieldView.on('input', () => {
273 this.borderWidth = borderWidthInput.fieldView.element.value;
274 });
275 // -- Color ---------------------------------------------------
276 const borderColorInput = new LabeledFieldView(locale, colorInputCreator);
277 borderColorInput.set({
278 label: t('Color'),
279 class: 'ck-table-form__border-color'
280 });
281 borderColorInput.fieldView.bind('value').to(this, 'borderColor');
282 borderColorInput.bind('isEnabled').to(this, 'borderStyle', isBorderStyleSet);
283 borderColorInput.fieldView.on('input', () => {
284 this.borderColor = borderColorInput.fieldView.value;
285 });
286 // Reset the border color and width fields depending on the `border-style` value.
287 this.on('change:borderStyle', (evt, name, newValue, oldValue) => {
288 // When removing the border (`border-style:none`), clear the remaining `border-*` properties.
289 // See: https://github.com/ckeditor/ckeditor5/issues/6227.
290 if (!isBorderStyleSet(newValue)) {
291 this.borderColor = '';
292 this.borderWidth = '';
293 }
294 // When setting the `border-style` from `none`, set the default `border-color` and `border-width` properties.
295 if (!isBorderStyleSet(oldValue)) {
296 this.borderColor = defaultBorder.color;
297 this.borderWidth = defaultBorder.width;
298 }
299 });
300 return {
301 borderRowLabel,
302 borderStyleDropdown,
303 borderColorInput,
304 borderWidthInput
305 };
306 }
307 /**
308 * Creates the following form fields:
309 *
310 * * {@link #backgroundInput}.
311 */
312 _createBackgroundFields() {
313 const locale = this.locale;
314 const t = this.t;
315 // -- Group label ---------------------------------------------
316 const backgroundRowLabel = new LabelView(locale);
317 backgroundRowLabel.text = t('Background');
318 // -- Background color input -----------------------------------
319 const colorInputCreator = getLabeledColorInputCreator({
320 colorConfig: this.options.backgroundColors,
321 columns: 5,
322 defaultColorValue: this.options.defaultTableCellProperties.backgroundColor
323 });
324 const backgroundInput = new LabeledFieldView(locale, colorInputCreator);
325 backgroundInput.set({
326 label: t('Color'),
327 class: 'ck-table-cell-properties-form__background'
328 });
329 backgroundInput.fieldView.bind('value').to(this, 'backgroundColor');
330 backgroundInput.fieldView.on('input', () => {
331 this.backgroundColor = backgroundInput.fieldView.value;
332 });
333 return {
334 backgroundRowLabel,
335 backgroundInput
336 };
337 }
338 /**
339 * Creates the following form fields:
340 *
341 * * {@link #widthInput}.
342 * * {@link #heightInput}.
343 */
344 _createDimensionFields() {
345 const locale = this.locale;
346 const t = this.t;
347 // -- Label ---------------------------------------------------
348 const dimensionsLabel = new LabelView(locale);
349 dimensionsLabel.text = t('Dimensions');
350 // -- Width ---------------------------------------------------
351 const widthInput = new LabeledFieldView(locale, createLabeledInputText);
352 widthInput.set({
353 label: t('Width'),
354 class: 'ck-table-form__dimensions-row__width'
355 });
356 widthInput.fieldView.bind('value').to(this, 'width');
357 widthInput.fieldView.on('input', () => {
358 this.width = widthInput.fieldView.element.value;
359 });
360 // -- Operator ---------------------------------------------------
361 const operatorLabel = new View(locale);
362 operatorLabel.setTemplate({
363 tag: 'span',
364 attributes: {
365 class: [
366 'ck-table-form__dimension-operator'
367 ]
368 },
369 children: [
370 { text: '×' }
371 ]
372 });
373 // -- Height ---------------------------------------------------
374 const heightInput = new LabeledFieldView(locale, createLabeledInputText);
375 heightInput.set({
376 label: t('Height'),
377 class: 'ck-table-form__dimensions-row__height'
378 });
379 heightInput.fieldView.bind('value').to(this, 'height');
380 heightInput.fieldView.on('input', () => {
381 this.height = heightInput.fieldView.element.value;
382 });
383 return {
384 dimensionsLabel,
385 widthInput,
386 operatorLabel,
387 heightInput
388 };
389 }
390 /**
391 * Creates the following form fields:
392 *
393 * * {@link #paddingInput}.
394 */
395 _createPaddingField() {
396 const locale = this.locale;
397 const t = this.t;
398 const paddingInput = new LabeledFieldView(locale, createLabeledInputText);
399 paddingInput.set({
400 label: t('Padding'),
401 class: 'ck-table-cell-properties-form__padding'
402 });
403 paddingInput.fieldView.bind('value').to(this, 'padding');
404 paddingInput.fieldView.on('input', () => {
405 this.padding = paddingInput.fieldView.element.value;
406 });
407 return paddingInput;
408 }
409 /**
410 * Creates the following form fields:
411 *
412 * * {@link #horizontalAlignmentToolbar},
413 * * {@link #verticalAlignmentToolbar}.
414 */
415 _createAlignmentFields() {
416 const locale = this.locale;
417 const t = this.t;
418 const alignmentLabel = new LabelView(locale);
419 alignmentLabel.text = t('Table cell text alignment');
420 // -- Horizontal ---------------------------------------------------
421 const horizontalAlignmentToolbar = new ToolbarView(locale);
422 const isContentRTL = locale.contentLanguageDirection === 'rtl';
423 horizontalAlignmentToolbar.set({
424 isCompact: true,
425 ariaLabel: t('Horizontal text alignment toolbar')
426 });
427 fillToolbar({
428 view: this,
429 icons: ALIGNMENT_ICONS,
430 toolbar: horizontalAlignmentToolbar,
431 labels: this._horizontalAlignmentLabels,
432 propertyName: 'horizontalAlignment',
433 nameToValue: name => {
434 // For the RTL content, we want to swap the buttons "align to the left" and "align to the right".
435 if (isContentRTL) {
436 if (name === 'left') {
437 return 'right';
438 }
439 else if (name === 'right') {
440 return 'left';
441 }
442 }
443 return name;
444 },
445 defaultValue: this.options.defaultTableCellProperties.horizontalAlignment
446 });
447 // -- Vertical -----------------------------------------------------
448 const verticalAlignmentToolbar = new ToolbarView(locale);
449 verticalAlignmentToolbar.set({
450 isCompact: true,
451 ariaLabel: t('Vertical text alignment toolbar')
452 });
453 fillToolbar({
454 view: this,
455 icons: ALIGNMENT_ICONS,
456 toolbar: verticalAlignmentToolbar,
457 labels: this._verticalAlignmentLabels,
458 propertyName: 'verticalAlignment',
459 defaultValue: this.options.defaultTableCellProperties.verticalAlignment
460 });
461 return {
462 horizontalAlignmentToolbar,
463 verticalAlignmentToolbar,
464 alignmentLabel
465 };
466 }
467 /**
468 * Creates the following form controls:
469 *
470 * * {@link #saveButtonView},
471 * * {@link #cancelButtonView}.
472 */
473 _createActionButtons() {
474 const locale = this.locale;
475 const t = this.t;
476 const saveButtonView = new ButtonView(locale);
477 const cancelButtonView = new ButtonView(locale);
478 const fieldsThatShouldValidateToSave = [
479 this.borderWidthInput,
480 this.borderColorInput,
481 this.backgroundInput,
482 this.paddingInput
483 ];
484 saveButtonView.set({
485 label: t('Save'),
486 icon: icons.check,
487 class: 'ck-button-save',
488 type: 'submit',
489 withText: true
490 });
491 saveButtonView.bind('isEnabled').toMany(fieldsThatShouldValidateToSave, 'errorText', (...errorTexts) => {
492 return errorTexts.every(errorText => !errorText);
493 });
494 cancelButtonView.set({
495 label: t('Cancel'),
496 icon: icons.cancel,
497 class: 'ck-button-cancel',
498 withText: true
499 });
500 cancelButtonView.delegate('execute').to(this, 'cancel');
501 return {
502 saveButtonView, cancelButtonView
503 };
504 }
505 /**
506 * Provides localized labels for {@link #horizontalAlignmentToolbar} buttons.
507 */
508 get _horizontalAlignmentLabels() {
509 const locale = this.locale;
510 const t = this.t;
511 const left = t('Align cell text to the left');
512 const center = t('Align cell text to the center');
513 const right = t('Align cell text to the right');
514 const justify = t('Justify cell text');
515 // Returns object with a proper order of labels.
516 if (locale.uiLanguageDirection === 'rtl') {
517 return { right, center, left, justify };
518 }
519 else {
520 return { left, center, right, justify };
521 }
522 }
523 /**
524 * Provides localized labels for {@link #verticalAlignmentToolbar} buttons.
525 */
526 get _verticalAlignmentLabels() {
527 const t = this.t;
528 return {
529 top: t('Align cell text to the top'),
530 middle: t('Align cell text to the middle'),
531 bottom: t('Align cell text to the bottom')
532 };
533 }
534}
535function isBorderStyleSet(value) {
536 return value !== 'none';
537}