mediaformview.js 8.8 KB

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