UNPKG

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