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 |
|
10 | import {
|
11 | ButtonView,
|
12 | FocusCycler,
|
13 | LabeledFieldView,
|
14 | View,
|
15 | ViewCollection,
|
16 | createLabeledInputText,
|
17 | injectCssTransitionDisabler,
|
18 | submitHandler
|
19 | } from 'ckeditor5/src/ui';
|
20 | import { FocusTracker, KeystrokeHandler } from 'ckeditor5/src/utils';
|
21 | import { icons } from 'ckeditor5/src/core';
|
22 |
|
23 | // See: #8833.
|
24 | // eslint-disable-next-line ckeditor5-rules/ckeditor-imports
|
25 | import '@ckeditor/ckeditor5-ui/theme/components/responsive-form/responsiveform.css';
|
26 | import '../../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 | */
|
35 | export 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 | */
|