UNPKG

9.32 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/ui/colorinputview
7 */
8import { View, InputTextView, ButtonView, createDropdown, ColorGridView, FocusCycler, ViewCollection } from 'ckeditor5/src/ui';
9import { icons } from 'ckeditor5/src/core';
10import { FocusTracker, KeystrokeHandler } from 'ckeditor5/src/utils';
11import '../../theme/colorinput.css';
12/**
13 * The color input view class. It allows the user to type in a color (hex, rgb, etc.)
14 * or choose it from the configurable color palette with a preview.
15 *
16 * @internal
17 */
18export default class ColorInputView extends View {
19 /**
20 * Creates an instance of the color input view.
21 *
22 * @param locale The locale instance.
23 * @param options The input options.
24 * @param options.colorDefinitions The colors to be displayed in the palette inside the input's dropdown.
25 * @param options.columns The number of columns in which the colors will be displayed.
26 * @param options.defaultColorValue If specified, the color input view will replace the "Remove color" button with
27 * the "Restore default" button. Instead of clearing the input field, the default color value will be set.
28 */
29 constructor(locale, options) {
30 super(locale);
31 this.set('value', '');
32 this.set('isReadOnly', false);
33 this.set('isFocused', false);
34 this.set('isEmpty', true);
35 this.options = options;
36 this.focusTracker = new FocusTracker();
37 this._focusables = new ViewCollection();
38 this.dropdownView = this._createDropdownView();
39 this.inputView = this._createInputTextView();
40 this.keystrokes = new KeystrokeHandler();
41 this._stillTyping = false;
42 this._focusCycler = new FocusCycler({
43 focusables: this._focusables,
44 focusTracker: this.focusTracker,
45 keystrokeHandler: this.keystrokes,
46 actions: {
47 // Navigate items backwards using the <kbd>Shift</kbd> + <kbd>Tab</kbd> keystroke.
48 focusPrevious: 'shift + tab',
49 // Navigate items forwards using the <kbd>Tab</kbd> key.
50 focusNext: 'tab'
51 }
52 });
53 this.setTemplate({
54 tag: 'div',
55 attributes: {
56 class: [
57 'ck',
58 'ck-input-color'
59 ]
60 },
61 children: [
62 this.dropdownView,
63 this.inputView
64 ]
65 });
66 this.on('change:value', (evt, name, inputValue) => this._setInputValue(inputValue));
67 }
68 /**
69 * @inheritDoc
70 */
71 render() {
72 super.render();
73 // Start listening for the keystrokes coming from the dropdown panel view.
74 this.keystrokes.listenTo(this.dropdownView.panelView.element);
75 }
76 /**
77 * Focuses the input.
78 */
79 focus() {
80 this.inputView.focus();
81 }
82 /**
83 * @inheritDoc
84 */
85 destroy() {
86 super.destroy();
87 this.focusTracker.destroy();
88 this.keystrokes.destroy();
89 }
90 /**
91 * Creates and configures the {@link #dropdownView}.
92 */
93 _createDropdownView() {
94 const locale = this.locale;
95 const t = locale.t;
96 const bind = this.bindTemplate;
97 const colorGrid = this._createColorGrid(locale);
98 const dropdown = createDropdown(locale);
99 const colorPreview = new View();
100 const removeColorButton = this._createRemoveColorButton();
101 colorPreview.setTemplate({
102 tag: 'span',
103 attributes: {
104 class: [
105 'ck',
106 'ck-input-color__button__preview'
107 ],
108 style: {
109 backgroundColor: bind.to('value')
110 }
111 },
112 children: [{
113 tag: 'span',
114 attributes: {
115 class: [
116 'ck',
117 'ck-input-color__button__preview__no-color-indicator',
118 bind.if('value', 'ck-hidden', value => value != '')
119 ]
120 }
121 }]
122 });
123 dropdown.buttonView.extendTemplate({
124 attributes: {
125 class: 'ck-input-color__button'
126 }
127 });
128 dropdown.buttonView.children.add(colorPreview);
129 dropdown.buttonView.label = t('Color picker');
130 dropdown.buttonView.tooltip = true;
131 dropdown.panelPosition = locale.uiLanguageDirection === 'rtl' ? 'se' : 'sw';
132 dropdown.panelView.children.add(removeColorButton);
133 dropdown.panelView.children.add(colorGrid);
134 dropdown.bind('isEnabled').to(this, 'isReadOnly', value => !value);
135 this._focusables.add(removeColorButton);
136 this._focusables.add(colorGrid);
137 this.focusTracker.add(removeColorButton.element);
138 this.focusTracker.add(colorGrid.element);
139 return dropdown;
140 }
141 /**
142 * Creates and configures an instance of {@link module:ui/inputtext/inputtextview~InputTextView}.
143 *
144 * @returns A configured instance to be set as {@link #inputView}.
145 */
146 _createInputTextView() {
147 const locale = this.locale;
148 const inputView = new InputTextView(locale);
149 inputView.extendTemplate({
150 on: {
151 blur: inputView.bindTemplate.to('blur')
152 }
153 });
154 inputView.value = this.value;
155 inputView.bind('isReadOnly', 'hasError').to(this);
156 this.bind('isFocused', 'isEmpty').to(inputView);
157 inputView.on('input', () => {
158 const inputValue = inputView.element.value;
159 // Check if the value matches one of our defined colors' label.
160 const mappedColor = this.options.colorDefinitions.find(def => inputValue === def.label);
161 this._stillTyping = true;
162 this.value = mappedColor && mappedColor.color || inputValue;
163 });
164 inputView.on('blur', () => {
165 this._stillTyping = false;
166 this._setInputValue(inputView.element.value);
167 });
168 inputView.delegate('input').to(this);
169 return inputView;
170 }
171 /**
172 * Creates and configures the button that clears the color.
173 */
174 _createRemoveColorButton() {
175 const locale = this.locale;
176 const t = locale.t;
177 const removeColorButton = new ButtonView(locale);
178 const defaultColor = this.options.defaultColorValue || '';
179 const removeColorButtonLabel = defaultColor ? t('Restore default') : t('Remove color');
180 removeColorButton.class = 'ck-input-color__remove-color';
181 removeColorButton.withText = true;
182 removeColorButton.icon = icons.eraser;
183 removeColorButton.label = removeColorButtonLabel;
184 removeColorButton.on('execute', () => {
185 this.value = defaultColor;
186 this.dropdownView.isOpen = false;
187 this.fire('input');
188 });
189 return removeColorButton;
190 }
191 /**
192 * Creates and configures the color grid inside the {@link #dropdownView}.
193 */
194 _createColorGrid(locale) {
195 const colorGrid = new ColorGridView(locale, {
196 colorDefinitions: this.options.colorDefinitions,
197 columns: this.options.columns
198 });
199 colorGrid.on('execute', (evtData, data) => {
200 this.value = data.value;
201 this.dropdownView.isOpen = false;
202 this.fire('input');
203 });
204 colorGrid.bind('selectedColor').to(this, 'value');
205 return colorGrid;
206 }
207 /**
208 * Sets {@link #inputView}'s value property to the color value or color label,
209 * if there is one and the user is not typing.
210 *
211 * Handles cases like:
212 *
213 * * Someone picks the color in the grid.
214 * * The color is set from the plugin level.
215 *
216 * @param inputValue Color value to be set.
217 */
218 _setInputValue(inputValue) {
219 if (!this._stillTyping) {
220 const normalizedInputValue = normalizeColor(inputValue);
221 // Check if the value matches one of our defined colors.
222 const mappedColor = this.options.colorDefinitions.find(def => normalizedInputValue === normalizeColor(def.color));
223 if (mappedColor) {
224 this.inputView.value = mappedColor.label;
225 }
226 else {
227 this.inputView.value = inputValue || '';
228 }
229 }
230 }
231}
232/**
233 * Normalizes color value, by stripping extensive whitespace.
234 * For example., transforms:
235 * * ` rgb( 25 50 0 )` to `rgb(25 50 0)`,
236 * * "\t rgb( 25 , 50,0 ) " to `rgb(25 50 0)`.
237 *
238 * @param colorString The value to be normalized.
239 */
240function normalizeColor(colorString) {
241 return colorString
242 // Remove any whitespace right after `(` or `,`.
243 .replace(/([(,])\s+/g, '$1')
244 // Remove any whitespace at the beginning or right before the end, `)`, `,`, or another whitespace.
245 .replace(/^\s+|\s+(?=[),\s]|$)/g, '')
246 // Then, replace `,` or whitespace with a single space.
247 .replace(/,|\s/g, ' ');
248}