1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 | import { Plugin } from 'ckeditor5/src/core';
|
9 | import { ButtonView, ContextualBalloon, clickOutsideHandler, getLocalizedColorOptions, normalizeColorOptions } from 'ckeditor5/src/ui';
|
10 | import { debounce } from 'lodash-es';
|
11 | import TablePropertiesView from './ui/tablepropertiesview';
|
12 | import tableProperties from './../../theme/icons/table-properties.svg';
|
13 | import { colorFieldValidator, getLocalizedColorErrorText, getLocalizedLengthErrorText, lengthFieldValidator, lineWidthFieldValidator, defaultColors } from '../utils/ui/table-properties';
|
14 | import { getTableWidgetAncestor } from '../utils/ui/widget';
|
15 | import { getBalloonTablePositionData, repositionContextualBalloon } from '../utils/ui/contextualballoon';
|
16 | import { getNormalizedDefaultProperties } from '../utils/table-properties';
|
17 | const ERROR_TEXT_TIMEOUT = 500;
|
18 |
|
19 | const propertyToCommandMap = {
|
20 | borderStyle: 'tableBorderStyle',
|
21 | borderColor: 'tableBorderColor',
|
22 | borderWidth: 'tableBorderWidth',
|
23 | backgroundColor: 'tableBackgroundColor',
|
24 | width: 'tableWidth',
|
25 | height: 'tableHeight',
|
26 | alignment: 'tableAlignment'
|
27 | };
|
28 |
|
29 |
|
30 |
|
31 |
|
32 |
|
33 |
|
34 | export default class TablePropertiesUI extends Plugin {
|
35 | |
36 |
|
37 |
|
38 | static get requires() {
|
39 | return [ContextualBalloon];
|
40 | }
|
41 | |
42 |
|
43 |
|
44 | static get pluginName() {
|
45 | return 'TablePropertiesUI';
|
46 | }
|
47 | |
48 |
|
49 |
|
50 | constructor(editor) {
|
51 | super(editor);
|
52 | |
53 |
|
54 |
|
55 | this.view = null;
|
56 | editor.config.define('table.tableProperties', {
|
57 | borderColors: defaultColors,
|
58 | backgroundColors: defaultColors
|
59 | });
|
60 | }
|
61 | |
62 |
|
63 |
|
64 | init() {
|
65 | const editor = this.editor;
|
66 | const t = editor.t;
|
67 | this._defaultTableProperties = getNormalizedDefaultProperties(editor.config.get('table.tableProperties.defaultProperties'), {
|
68 | includeAlignmentProperty: true
|
69 | });
|
70 | this._balloon = editor.plugins.get(ContextualBalloon);
|
71 | editor.ui.componentFactory.add('tableProperties', locale => {
|
72 | const view = new ButtonView(locale);
|
73 | view.set({
|
74 | label: t('Table properties'),
|
75 | icon: tableProperties,
|
76 | tooltip: true
|
77 | });
|
78 | this.listenTo(view, 'execute', () => this._showView());
|
79 | const commands = Object.values(propertyToCommandMap)
|
80 | .map(commandName => editor.commands.get(commandName));
|
81 | view.bind('isEnabled').toMany(commands, 'isEnabled', (...areEnabled) => (areEnabled.some(isCommandEnabled => isCommandEnabled)));
|
82 | return view;
|
83 | });
|
84 | }
|
85 | |
86 |
|
87 |
|
88 | destroy() {
|
89 | super.destroy();
|
90 |
|
91 |
|
92 | if (this.view) {
|
93 | this.view.destroy();
|
94 | }
|
95 | }
|
96 | |
97 |
|
98 |
|
99 |
|
100 |
|
101 | _createPropertiesView() {
|
102 | const editor = this.editor;
|
103 | const config = editor.config.get('table.tableProperties');
|
104 | const borderColorsConfig = normalizeColorOptions(config.borderColors);
|
105 | const localizedBorderColors = getLocalizedColorOptions(editor.locale, borderColorsConfig);
|
106 | const backgroundColorsConfig = normalizeColorOptions(config.backgroundColors);
|
107 | const localizedBackgroundColors = getLocalizedColorOptions(editor.locale, backgroundColorsConfig);
|
108 | const view = new TablePropertiesView(editor.locale, {
|
109 | borderColors: localizedBorderColors,
|
110 | backgroundColors: localizedBackgroundColors,
|
111 | defaultTableProperties: this._defaultTableProperties
|
112 | });
|
113 | const t = editor.t;
|
114 |
|
115 | view.render();
|
116 | this.listenTo(view, 'submit', () => {
|
117 | this._hideView();
|
118 | });
|
119 | this.listenTo(view, 'cancel', () => {
|
120 |
|
121 | if (this._undoStepBatch.operations.length) {
|
122 | editor.execute('undo', this._undoStepBatch);
|
123 | }
|
124 | this._hideView();
|
125 | });
|
126 |
|
127 | view.keystrokes.set('Esc', (data, cancel) => {
|
128 | this._hideView();
|
129 | cancel();
|
130 | });
|
131 |
|
132 | clickOutsideHandler({
|
133 | emitter: view,
|
134 | activator: () => this._isViewInBalloon,
|
135 | contextElements: [this._balloon.view.element],
|
136 | callback: () => this._hideView()
|
137 | });
|
138 | const colorErrorText = getLocalizedColorErrorText(t);
|
139 | const lengthErrorText = getLocalizedLengthErrorText(t);
|
140 |
|
141 |
|
142 |
|
143 |
|
144 |
|
145 | view.on('change:borderStyle', this._getPropertyChangeCallback('tableBorderStyle'));
|
146 | view.on('change:borderColor', this._getValidatedPropertyChangeCallback({
|
147 | viewField: view.borderColorInput,
|
148 | commandName: 'tableBorderColor',
|
149 | errorText: colorErrorText,
|
150 | validator: colorFieldValidator
|
151 | }));
|
152 | view.on('change:borderWidth', this._getValidatedPropertyChangeCallback({
|
153 | viewField: view.borderWidthInput,
|
154 | commandName: 'tableBorderWidth',
|
155 | errorText: lengthErrorText,
|
156 | validator: lineWidthFieldValidator
|
157 | }));
|
158 | view.on('change:backgroundColor', this._getValidatedPropertyChangeCallback({
|
159 | viewField: view.backgroundInput,
|
160 | commandName: 'tableBackgroundColor',
|
161 | errorText: colorErrorText,
|
162 | validator: colorFieldValidator
|
163 | }));
|
164 | view.on('change:width', this._getValidatedPropertyChangeCallback({
|
165 | viewField: view.widthInput,
|
166 | commandName: 'tableWidth',
|
167 | errorText: lengthErrorText,
|
168 | validator: lengthFieldValidator
|
169 | }));
|
170 | view.on('change:height', this._getValidatedPropertyChangeCallback({
|
171 | viewField: view.heightInput,
|
172 | commandName: 'tableHeight',
|
173 | errorText: lengthErrorText,
|
174 | validator: lengthFieldValidator
|
175 | }));
|
176 | view.on('change:alignment', this._getPropertyChangeCallback('tableAlignment'));
|
177 | return view;
|
178 | }
|
179 | |
180 |
|
181 |
|
182 |
|
183 |
|
184 |
|
185 |
|
186 |
|
187 | _fillViewFormFromCommandValues() {
|
188 | const commands = this.editor.commands;
|
189 | const borderStyleCommand = commands.get('tableBorderStyle');
|
190 | Object.entries(propertyToCommandMap)
|
191 | .map(([property, commandName]) => {
|
192 | const propertyKey = property;
|
193 | const defaultValue = this._defaultTableProperties[propertyKey] || '';
|
194 | return [propertyKey, (commands.get(commandName).value || defaultValue)];
|
195 | })
|
196 | .forEach(([property, value]) => {
|
197 |
|
198 | if ((property === 'borderColor' || property === 'borderWidth') && borderStyleCommand.value === 'none') {
|
199 | return;
|
200 | }
|
201 | this.view.set(property, value);
|
202 | });
|
203 | this._isReady = true;
|
204 | }
|
205 | |
206 |
|
207 |
|
208 |
|
209 |
|
210 |
|
211 |
|
212 | _showView() {
|
213 | const editor = this.editor;
|
214 | if (!this.view) {
|
215 | this.view = this._createPropertiesView();
|
216 | }
|
217 | this.listenTo(editor.ui, 'update', () => {
|
218 | this._updateView();
|
219 | });
|
220 |
|
221 | this._fillViewFormFromCommandValues();
|
222 | this._balloon.add({
|
223 | view: this.view,
|
224 | position: getBalloonTablePositionData(editor)
|
225 | });
|
226 |
|
227 | this._undoStepBatch = editor.model.createBatch();
|
228 |
|
229 | this.view.focus();
|
230 | }
|
231 | |
232 |
|
233 |
|
234 | _hideView() {
|
235 | const editor = this.editor;
|
236 | this.stopListening(editor.ui, 'update');
|
237 | this._isReady = false;
|
238 |
|
239 |
|
240 | this.view.saveButtonView.focus();
|
241 | this._balloon.remove(this.view);
|
242 |
|
243 |
|
244 | this.editor.editing.view.focus();
|
245 | }
|
246 | |
247 |
|
248 |
|
249 | _updateView() {
|
250 | const editor = this.editor;
|
251 | const viewDocument = editor.editing.view.document;
|
252 | if (!getTableWidgetAncestor(viewDocument.selection)) {
|
253 | this._hideView();
|
254 | }
|
255 | else if (this._isViewVisible) {
|
256 | repositionContextualBalloon(editor, 'table');
|
257 | }
|
258 | }
|
259 | |
260 |
|
261 |
|
262 | get _isViewVisible() {
|
263 | return !!this.view && this._balloon.visibleView === this.view;
|
264 | }
|
265 | |
266 |
|
267 |
|
268 | get _isViewInBalloon() {
|
269 | return !!this.view && this._balloon.hasView(this.view);
|
270 | }
|
271 | |
272 |
|
273 |
|
274 |
|
275 |
|
276 |
|
277 |
|
278 |
|
279 | _getPropertyChangeCallback(commandName) {
|
280 | return (evt, propertyName, newValue) => {
|
281 |
|
282 | if (!this._isReady) {
|
283 | return;
|
284 | }
|
285 | this.editor.execute(commandName, {
|
286 | value: newValue,
|
287 | batch: this._undoStepBatch
|
288 | });
|
289 | };
|
290 | }
|
291 | |
292 |
|
293 |
|
294 |
|
295 |
|
296 | _getValidatedPropertyChangeCallback(options) {
|
297 | const { commandName, viewField, validator, errorText } = options;
|
298 | const setErrorTextDebounced = debounce(() => {
|
299 | viewField.errorText = errorText;
|
300 | }, ERROR_TEXT_TIMEOUT);
|
301 | return (evt, propertyName, newValue) => {
|
302 | setErrorTextDebounced.cancel();
|
303 |
|
304 | if (!this._isReady) {
|
305 | return;
|
306 | }
|
307 | if (validator(newValue)) {
|
308 | this.editor.execute(commandName, {
|
309 | value: newValue,
|
310 | batch: this._undoStepBatch
|
311 | });
|
312 | viewField.errorText = null;
|
313 | }
|
314 | else {
|
315 | setErrorTextDebounced();
|
316 | }
|
317 | };
|
318 | }
|
319 | }
|