8
0

mediaformview.js 9.0 KB

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