UNPKG

8.86 kBJavaScriptView Raw
1/**
2 * @license Copyright (c) 2003-2022, 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/**
7 * @module media-embed/ui/mediaformview
8 */
9
10import {
11 ButtonView,
12 FocusCycler,
13 LabeledFieldView,
14 View,
15 ViewCollection,
16 createLabeledInputText,
17 injectCssTransitionDisabler,
18 submitHandler
19} from 'ckeditor5/src/ui';
20import { FocusTracker, KeystrokeHandler } from 'ckeditor5/src/utils';
21import { icons } from 'ckeditor5/src/core';
22
23// See: #8833.
24// eslint-disable-next-line ckeditor5-rules/ckeditor-imports
25import '@ckeditor/ckeditor5-ui/theme/components/responsive-form/responsiveform.css';
26import '../../theme/mediaform.css';
27
28/**
29 * The media form view controller class.
30 *
31 * See {@link module:media-embed/ui/mediaformview~MediaFormView}.
32 *
33 * @extends module:ui/view~View
34 */
35export default class MediaFormView extends View {
36 /**
37 * @param {Array.<Function>} validators Form validators used by {@link #isValid}.
38 * @param {module:utils/locale~Locale} [locale] The localization services instance.
39 */
40 constructor( validators, locale ) {
41 super( locale );
42
43 const t = locale.t;
44
45 /**
46 * Tracks information about the DOM focus in the form.
47 *
48 * @readonly
49 * @member {module:utils/focustracker~FocusTracker}
50 */
51 this.focusTracker = new FocusTracker();
52
53 /**
54 * An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
55 *
56 * @readonly
57 * @member {module:utils/keystrokehandler~KeystrokeHandler}
58 */
59 this.keystrokes = new KeystrokeHandler();
60
61 /**
62 * The value of the URL input.
63 *
64 * @member {String} #mediaURLInputValue
65 * @observable
66 */
67 this.set( 'mediaURLInputValue', '' );
68
69 /**
70 * The URL input view.
71 *
72 * @member {module:ui/labeledfield/labeledfieldview~LabeledFieldView}
73 */
74 this.urlInputView = this._createUrlInput();
75
76 /**
77 * The Save button view.
78 *
79 * @member {module:ui/button/buttonview~ButtonView}
80 */
81 this.saveButtonView = this._createButton( t( 'Save' ), icons.check, 'ck-button-save' );
82 this.saveButtonView.type = 'submit';
83 this.saveButtonView.bind( 'isEnabled' ).to( this, 'mediaURLInputValue', value => !!value );
84
85 /**
86 * The Cancel button view.
87 *
88 * @member {module:ui/button/buttonview~ButtonView}
89 */
90 this.cancelButtonView = this._createButton( t( 'Cancel' ), icons.cancel, 'ck-button-cancel', 'cancel' );
91
92 /**
93 * A collection of views that can be focused in the form.
94 *
95 * @readonly
96 * @protected
97 * @member {module:ui/viewcollection~ViewCollection}
98 */
99 this._focusables = new ViewCollection();
100
101 /**
102 * Helps cycling over {@link #_focusables} in the form.
103 *
104 * @readonly
105 * @protected
106 * @member {module:ui/focuscycler~FocusCycler}
107 */
108 this._focusCycler = new FocusCycler( {
109 focusables: this._focusables,
110 focusTracker: this.focusTracker,
111 keystrokeHandler: this.keystrokes,
112 actions: {
113 // Navigate form fields backwards using the <kbd>Shift</kbd> + <kbd>Tab</kbd> keystroke.
114 focusPrevious: 'shift + tab',
115
116 // Navigate form fields forwards using the <kbd>Tab</kbd> key.
117 focusNext: 'tab'
118 }
119 } );
120
121 /**
122 * An array of form validators used by {@link #isValid}.
123 *
124 * @readonly
125 * @protected
126 * @member {Array.<Function>}
127 */
128 this._validators = validators;
129
130 this.setTemplate( {
131 tag: 'form',
132
133 attributes: {
134 class: [
135 'ck',
136 'ck-media-form',
137 'ck-responsive-form'
138 ],
139
140 tabindex: '-1'
141 },
142
143 children: [
144 this.urlInputView,
145 this.saveButtonView,
146 this.cancelButtonView
147 ]
148 } );
149
150 injectCssTransitionDisabler( this );
151
152 /**
153 * The default info text for the {@link #urlInputView}.
154 *
155 * @private
156 * @member {String} #_urlInputViewInfoDefault
157 */
158
159 /**
160 * The info text with an additional tip for the {@link #urlInputView},
161 * displayed when the input has some value.
162 *
163 * @private
164 * @member {String} #_urlInputViewInfoTip
165 */
166 }
167
168 /**
169 * @inheritDoc
170 */
171 render() {
172 super.render();
173
174 submitHandler( {
175 view: this
176 } );
177
178 const childViews = [
179 this.urlInputView,
180 this.saveButtonView,
181 this.cancelButtonView
182 ];
183
184 childViews.forEach( v => {
185 // Register the view as focusable.
186 this._focusables.add( v );
187
188 // Register the view in the focus tracker.
189 this.focusTracker.add( v.element );
190 } );
191
192 // Start listening for the keystrokes coming from #element.
193 this.keystrokes.listenTo( this.element );
194
195 const stopPropagation = data => data.stopPropagation();
196
197 // Since the form is in the dropdown panel which is a child of the toolbar, the toolbar's
198 // keystroke handler would take over the key management in the URL input. We need to prevent
199 // this ASAP. Otherwise, the basic caret movement using the arrow keys will be impossible.
200 this.keystrokes.set( 'arrowright', stopPropagation );
201 this.keystrokes.set( 'arrowleft', stopPropagation );
202 this.keystrokes.set( 'arrowup', stopPropagation );
203 this.keystrokes.set( 'arrowdown', stopPropagation );
204
205 // Intercept the `selectstart` event, which is blocked by default because of the default behavior
206 // of the DropdownView#panelView.
207 // TODO: blocking `selectstart` in the #panelView should be configurable per–drop–down instance.
208 this.listenTo( this.urlInputView.element, 'selectstart', ( evt, domEvt ) => {
209 domEvt.stopPropagation();
210 }, { priority: 'high' } );
211 }
212
213 /**
214 * @inheritDoc
215 */
216 destroy() {
217 super.destroy();
218
219 this.focusTracker.destroy();
220 this.keystrokes.destroy();
221 }
222
223 /**
224 * Focuses the fist {@link #_focusables} in the form.
225 */
226 focus() {
227 this._focusCycler.focusFirst();
228 }
229
230 /**
231 * The native DOM `value` of the {@link #urlInputView} element.
232 *
233 * **Note**: Do not confuse it with the {@link module:ui/inputtext/inputtextview~InputTextView#value}
234 * which works one way only and may not represent the actual state of the component in the DOM.
235 *
236 * @type {String}
237 */
238 get url() {
239 return this.urlInputView.fieldView.element.value.trim();
240 }
241
242 set url( url ) {
243 this.urlInputView.fieldView.element.value = url.trim();
244 }
245
246 /**
247 * Validates the form and returns `false` when some fields are invalid.
248 *
249 * @returns {Boolean}
250 */
251 isValid() {
252 this.resetFormStatus();
253
254 for ( const validator of this._validators ) {
255 const errorText = validator( this );
256
257 // One error per field is enough.
258 if ( errorText ) {
259 // Apply updated error.
260 this.urlInputView.errorText = errorText;
261
262 return false;
263 }
264 }
265
266 return true;
267 }
268
269 /**
270 * Cleans up the supplementary error and information text of the {@link #urlInputView}
271 * bringing them back to the state when the form has been displayed for the first time.
272 *
273 * See {@link #isValid}.
274 */
275 resetFormStatus() {
276 this.urlInputView.errorText = null;
277 this.urlInputView.infoText = this._urlInputViewInfoDefault;
278 }
279
280 /**
281 * Creates a labeled input view.
282 *
283 * @private
284 * @returns {module:ui/labeledfield/labeledfieldview~LabeledFieldView} Labeled input view instance.
285 */
286 _createUrlInput() {
287 const t = this.locale.t;
288
289 const labeledInput = new LabeledFieldView( this.locale, createLabeledInputText );
290 const inputField = labeledInput.fieldView;
291
292 this._urlInputViewInfoDefault = t( 'Paste the media URL in the input.' );
293 this._urlInputViewInfoTip = t( 'Tip: Paste the URL into the content to embed faster.' );
294
295 labeledInput.label = t( 'Media URL' );
296 labeledInput.infoText = this._urlInputViewInfoDefault;
297
298 inputField.on( 'input', () => {
299 // Display the tip text only when there is some value. Otherwise fall back to the default info text.
300 labeledInput.infoText = inputField.element.value ? this._urlInputViewInfoTip : this._urlInputViewInfoDefault;
301 this.mediaURLInputValue = inputField.element.value.trim();
302 } );
303
304 return labeledInput;
305 }
306
307 /**
308 * Creates a button view.
309 *
310 * @private
311 * @param {String} label The button label.
312 * @param {String} icon The button icon.
313 * @param {String} className The additional button CSS class name.
314 * @param {String} [eventName] An event name that the `ButtonView#execute` event will be delegated to.
315 * @returns {module:ui/button/buttonview~ButtonView} The button view instance.
316 */
317 _createButton( label, icon, className, eventName ) {
318 const button = new ButtonView( this.locale );
319
320 button.set( {
321 label,
322 icon,
323 tooltip: true
324 } );
325
326 button.extendTemplate( {
327 attributes: {
328 class: className
329 }
330 } );
331
332 if ( eventName ) {
333 button.delegate( 'execute' ).to( this, eventName );
334 }
335
336 return button;
337 }
338}
339
340/**
341 * Fired when the form view is submitted (when one of the children triggered the submit event),
342 * e.g. click on {@link #saveButtonView}.
343 *
344 * @event submit
345 */
346
347/**
348 * Fired when the form view is canceled, e.g. by a click on {@link #cancelButtonView}.
349 *
350 * @event cancel
351 */