/** * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ /** * @module media-embed/ui/mediaformview */ import View from '@ckeditor/ckeditor5-ui/src/view'; import ViewCollection from '@ckeditor/ckeditor5-ui/src/viewcollection'; import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview'; import LabeledFieldView from '@ckeditor/ckeditor5-ui/src/labeledfield/labeledfieldview'; import { createLabeledInputText } from '@ckeditor/ckeditor5-ui/src/labeledfield/utils'; import submitHandler from '@ckeditor/ckeditor5-ui/src/bindings/submithandler'; import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker'; import FocusCycler from '@ckeditor/ckeditor5-ui/src/focuscycler'; import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler'; import checkIcon from '@ckeditor/ckeditor5-core/theme/icons/check.svg'; import cancelIcon from '@ckeditor/ckeditor5-core/theme/icons/cancel.svg'; import '../../theme/mediaform.css'; import '@ckeditor/ckeditor5-ui/theme/components/responsive-form/responsiveform.css'; /** * The media form view controller class. * * See {@link module:media-embed/ui/mediaformview~MediaFormView}. * * @extends module:ui/view~View */ export default class MediaFormView extends View { /** * @param {Array.} validators Form validators used by {@link #isValid}. * @param {module:utils/locale~Locale} [locale] The localization services instance. */ constructor( validators, locale ) { super( locale ); const t = locale.t; /** * Tracks information about the DOM focus in the form. * * @readonly * @member {module:utils/focustracker~FocusTracker} */ this.focusTracker = new FocusTracker(); /** * An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}. * * @readonly * @member {module:utils/keystrokehandler~KeystrokeHandler} */ this.keystrokes = new KeystrokeHandler(); /** * The value of the URL input. * * @member {String} #mediaURLInputValue * @observable */ this.set( 'mediaURLInputValue', '' ); /** * The URL input view. * * @member {module:ui/labeledfield/labeledfieldview~LabeledFieldView} */ this.urlInputView = this._createUrlInput(); /** * The Save button view. * * @member {module:ui/button/buttonview~ButtonView} */ this.saveButtonView = this._createButton( t( 'Save' ), checkIcon, 'ck-button-save' ); this.saveButtonView.type = 'submit'; this.saveButtonView.bind( 'isEnabled' ).to( this, 'mediaURLInputValue', value => !!value ); /** * The Cancel button view. * * @member {module:ui/button/buttonview~ButtonView} */ this.cancelButtonView = this._createButton( t( 'Cancel' ), cancelIcon, 'ck-button-cancel', 'cancel' ); /** * A collection of views that can be focused in the form. * * @readonly * @protected * @member {module:ui/viewcollection~ViewCollection} */ this._focusables = new ViewCollection(); /** * Helps cycling over {@link #_focusables} in the form. * * @readonly * @protected * @member {module:ui/focuscycler~FocusCycler} */ this._focusCycler = new FocusCycler( { focusables: this._focusables, focusTracker: this.focusTracker, keystrokeHandler: this.keystrokes, actions: { // Navigate form fields backwards using the Shift + Tab keystroke. focusPrevious: 'shift + tab', // Navigate form fields forwards using the Tab key. focusNext: 'tab' } } ); /** * An array of form validators used by {@link #isValid}. * * @readonly * @protected * @member {Array.} */ this._validators = validators; this.setTemplate( { tag: 'form', attributes: { class: [ 'ck', 'ck-media-form', 'ck-responsive-form' ], tabindex: '-1' }, children: [ this.urlInputView, this.saveButtonView, this.cancelButtonView ] } ); /** * The default info text for the {@link #urlInputView}. * * @private * @member {String} #_urlInputViewInfoDefault */ /** * The info text with an additional tip for the {@link #urlInputView}, * displayed when the input has some value. * * @private * @member {String} #_urlInputViewInfoTip */ } /** * @inheritDoc */ render() { super.render(); submitHandler( { view: this } ); const childViews = [ this.urlInputView, this.saveButtonView, this.cancelButtonView ]; childViews.forEach( v => { // Register the view as focusable. this._focusables.add( v ); // Register the view in the focus tracker. this.focusTracker.add( v.element ); } ); // Start listening for the keystrokes coming from #element. this.keystrokes.listenTo( this.element ); const stopPropagation = data => data.stopPropagation(); // Since the form is in the dropdown panel which is a child of the toolbar, the toolbar's // keystroke handler would take over the key management in the URL input. We need to prevent // this ASAP. Otherwise, the basic caret movement using the arrow keys will be impossible. this.keystrokes.set( 'arrowright', stopPropagation ); this.keystrokes.set( 'arrowleft', stopPropagation ); this.keystrokes.set( 'arrowup', stopPropagation ); this.keystrokes.set( 'arrowdown', stopPropagation ); // Intercept the `selectstart` event, which is blocked by default because of the default behavior // of the DropdownView#panelView. // TODO: blocking `selectstart` in the #panelView should be configurable per–drop–down instance. this.listenTo( this.urlInputView.element, 'selectstart', ( evt, domEvt ) => { domEvt.stopPropagation(); }, { priority: 'high' } ); } /** * Focuses the fist {@link #_focusables} in the form. */ focus() { this._focusCycler.focusFirst(); } /** * The native DOM `value` of the {@link #urlInputView} element. * * **Note**: Do not confuse it with the {@link module:ui/inputtext/inputtextview~InputTextView#value} * which works one way only and may not represent the actual state of the component in the DOM. * * @type {String} */ get url() { return this.urlInputView.fieldView.element.value.trim(); } set url( url ) { this.urlInputView.fieldView.element.value = url.trim(); } /** * Validates the form and returns `false` when some fields are invalid. * * @returns {Boolean} */ isValid() { this.resetFormStatus(); for ( const validator of this._validators ) { const errorText = validator( this ); // One error per field is enough. if ( errorText ) { // Apply updated error. this.urlInputView.errorText = errorText; return false; } } return true; } /** * Cleans up the supplementary error and information text of the {@link #urlInputView} * bringing them back to the state when the form has been displayed for the first time. * * See {@link #isValid}. */ resetFormStatus() { this.urlInputView.errorText = null; this.urlInputView.infoText = this._urlInputViewInfoDefault; } /** * Creates a labeled input view. * * @private * @returns {module:ui/labeledfield/labeledfieldview~LabeledFieldView} Labeled input view instance. */ _createUrlInput() { const t = this.locale.t; const labeledInput = new LabeledFieldView( this.locale, createLabeledInputText ); const inputField = labeledInput.fieldView; this._urlInputViewInfoDefault = t( 'Paste the media URL in the input.' ); this._urlInputViewInfoTip = t( 'Tip: Paste the URL into the content to embed faster.' ); labeledInput.label = t( 'Media URL' ); labeledInput.infoText = this._urlInputViewInfoDefault; inputField.placeholder = 'https://example.com'; inputField.on( 'input', () => { // Display the tip text only when there is some value. Otherwise fall back to the default info text. labeledInput.infoText = inputField.element.value ? this._urlInputViewInfoTip : this._urlInputViewInfoDefault; this.mediaURLInputValue = inputField.element.value.trim(); } ); return labeledInput; } /** * Creates a button view. * * @private * @param {String} label The button label. * @param {String} icon The button icon. * @param {String} className The additional button CSS class name. * @param {String} [eventName] An event name that the `ButtonView#execute` event will be delegated to. * @returns {module:ui/button/buttonview~ButtonView} The button view instance. */ _createButton( label, icon, className, eventName ) { const button = new ButtonView( this.locale ); button.set( { label, icon, tooltip: true } ); button.extendTemplate( { attributes: { class: className } } ); if ( eventName ) { button.delegate( 'execute' ).to( this, eventName ); } return button; } } /** * Fired when the form view is submitted (when one of the children triggered the submit event), * e.g. click on {@link #saveButtonView}. * * @event submit */ /** * Fired when the form view is canceled, e.g. by a click on {@link #cancelButtonView}. * * @event cancel */