link.js 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. /**
  2. * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module link/link
  7. */
  8. import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
  9. import ClickObserver from '@ckeditor/ckeditor5-engine/src/view/observer/clickobserver';
  10. import LinkEngine from './linkengine';
  11. import LinkElement from './linkelement';
  12. import clickOutsideHandler from '@ckeditor/ckeditor5-ui/src/bindings/clickoutsidehandler';
  13. import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
  14. import BalloonPanelView from '@ckeditor/ckeditor5-ui/src/panel/balloon/balloonpanelview';
  15. import LinkFormView from './ui/linkformview';
  16. import linkIcon from '../theme/icons/link.svg';
  17. import unlinkIcon from '../theme/icons/unlink.svg';
  18. import '../theme/theme.scss';
  19. /**
  20. * The link feature. It introduces the Link and Unlink buttons and the <kbd>Ctrl+K</kbd> keystroke.
  21. *
  22. * It uses the {@link module:link/linkengine~LinkEngine link engine feature}.
  23. *
  24. * @extends module:core/plugin~Plugin
  25. */
  26. export default class Link extends Plugin {
  27. /**
  28. * @inheritDoc
  29. */
  30. static get requires() {
  31. return [ LinkEngine ];
  32. }
  33. /**
  34. * @inheritDoc
  35. */
  36. init() {
  37. this.editor.editing.view.addObserver( ClickObserver );
  38. /**
  39. * Balloon panel view to display the main UI.
  40. *
  41. * @member {module:link/ui/balloonpanel~BalloonPanelView}
  42. */
  43. this.balloonPanelView = this._createBalloonPanel();
  44. /**
  45. * The form view inside {@link #balloonPanelView}.
  46. *
  47. * @member {module:link/ui/linkformview~LinkFormView}
  48. */
  49. this.formView = this._createForm();
  50. // Create toolbar buttons.
  51. this._createToolbarLinkButton();
  52. this._createToolbarUnlinkButton();
  53. }
  54. /**
  55. * Creates a toolbar link button. Clicking this button will show
  56. * {@link #balloonPanelView} attached to the selection.
  57. *
  58. * @private
  59. */
  60. _createToolbarLinkButton() {
  61. const editor = this.editor;
  62. const linkCommand = editor.commands.get( 'link' );
  63. const t = editor.t;
  64. // Handle `Ctrl+K` keystroke and show panel.
  65. editor.keystrokes.set( 'CTRL+K', () => this._showPanel() );
  66. editor.ui.componentFactory.add( 'link', ( locale ) => {
  67. const button = new ButtonView( locale );
  68. button.isEnabled = true;
  69. button.label = t( 'Link' );
  70. button.icon = linkIcon;
  71. button.keystroke = 'CTRL+K';
  72. button.tooltip = true;
  73. // Bind button to the command.
  74. button.bind( 'isEnabled' ).to( linkCommand, 'isEnabled' );
  75. // Show the panel on button click.
  76. this.listenTo( button, 'execute', () => this._showPanel() );
  77. return button;
  78. } );
  79. }
  80. /**
  81. * Creates a toolbar unlink button. Clicking this button will unlink
  82. * the selected link.
  83. *
  84. * @private
  85. */
  86. _createToolbarUnlinkButton() {
  87. const editor = this.editor;
  88. const t = editor.t;
  89. const unlinkCommand = editor.commands.get( 'unlink' );
  90. editor.ui.componentFactory.add( 'unlink', ( locale ) => {
  91. const button = new ButtonView( locale );
  92. button.isEnabled = false;
  93. button.label = t( 'Unlink' );
  94. button.icon = unlinkIcon;
  95. button.tooltip = true;
  96. // Bind button to the command.
  97. button.bind( 'isEnabled' ).to( unlinkCommand, 'isEnabled' );
  98. // Execute unlink command and hide panel, if open on button click.
  99. this.listenTo( button, 'execute', () => editor.execute( 'unlink' ) );
  100. return button;
  101. } );
  102. }
  103. /**
  104. * Creates the {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView} instance.
  105. *
  106. * @private
  107. * @returns {module:ui/panel/balloon/balloonpanelview~BalloonPanelView} Link balloon panel instance.
  108. */
  109. _createBalloonPanel() {
  110. const editor = this.editor;
  111. const viewDocument = editor.editing.view;
  112. // Create the balloon panel instance.
  113. const balloonPanelView = new BalloonPanelView( editor.locale );
  114. balloonPanelView.maxWidth = 300;
  115. // Add balloonPanel.view#element to FocusTracker.
  116. // @TODO: Do it automatically ckeditor5-core#23
  117. editor.ui.focusTracker.add( balloonPanelView.element );
  118. // Handle click on view document and show panel when selection is placed inside the link element.
  119. // Keep panel open until selection will be inside the same link element.
  120. this.listenTo( viewDocument, 'click', () => {
  121. const viewSelection = viewDocument.selection;
  122. const parentLink = getPositionParentLink( viewSelection.getFirstPosition() );
  123. if ( viewSelection.isCollapsed && parentLink ) {
  124. this._attachPanelToElement();
  125. this.listenTo( viewDocument, 'render', () => {
  126. const currentParentLink = getPositionParentLink( viewSelection.getFirstPosition() );
  127. if ( !viewSelection.isCollapsed || parentLink !== currentParentLink ) {
  128. this._hidePanel();
  129. } else {
  130. this._attachPanelToElement( parentLink );
  131. }
  132. } );
  133. this.listenTo( balloonPanelView, 'change:isVisible', () => this.stopListening( viewDocument, 'render' ) );
  134. }
  135. } );
  136. // Focus the form if balloon panel is open and tab key has been pressed.
  137. editor.keystrokes.set( 'Tab', ( data, cancel ) => {
  138. if ( balloonPanelView.isVisible && !this.formView.focusTracker.isFocused ) {
  139. this.formView.focus();
  140. cancel();
  141. }
  142. } );
  143. // Close the panel on esc key press when editable has focus.
  144. editor.keystrokes.set( 'Esc', ( data, cancel ) => {
  145. if ( balloonPanelView.isVisible ) {
  146. this._hidePanel( true );
  147. cancel();
  148. }
  149. } );
  150. // Close on click outside of balloon panel element.
  151. clickOutsideHandler( {
  152. emitter: balloonPanelView,
  153. activator: () => balloonPanelView.isVisible,
  154. contextElement: balloonPanelView.element,
  155. callback: () => this._hidePanel()
  156. } );
  157. editor.ui.view.body.add( balloonPanelView );
  158. return balloonPanelView;
  159. }
  160. /**
  161. * Creates the {@link module:link/ui/linkformview~LinkFormView} instance.
  162. *
  163. * @private
  164. * @returns {module:link/ui/linkformview~LinkFormView} Link form instance.
  165. */
  166. _createForm() {
  167. const editor = this.editor;
  168. const formView = new LinkFormView( editor.locale );
  169. formView.urlInputView.bind( 'value' ).to( editor.commands.get( 'link' ), 'value' );
  170. // Execute link command after clicking on formView `Save` button.
  171. this.listenTo( formView, 'submit', () => {
  172. editor.execute( 'link', formView.urlInputView.inputView.element.value );
  173. this._hidePanel( true );
  174. } );
  175. // Execute unlink command after clicking on formView `Unlink` button.
  176. this.listenTo( formView, 'unlink', () => {
  177. editor.execute( 'unlink' );
  178. this._hidePanel( true );
  179. } );
  180. // Close the panel on esc key press when the form has focus.
  181. formView.keystrokes.set( 'Esc', ( data, cancel ) => {
  182. this._hidePanel( true );
  183. cancel();
  184. } );
  185. // Hide balloon panel after clicking on formView `Cancel` button.
  186. this.listenTo( formView, 'cancel', () => this._hidePanel( true ) );
  187. this.balloonPanelView.content.add( formView );
  188. return formView;
  189. }
  190. /**
  191. * Shows {@link #balloonPanelView link balloon panel} and attach to target element.
  192. * If selection is collapsed and is placed inside link element, then panel will be attached
  193. * to whole link element, otherwise will be attached to the selection.
  194. *
  195. * @private
  196. * @param {module:link/linkelement~LinkElement} [parentLink] Target element.
  197. */
  198. _attachPanelToElement( parentLink ) {
  199. const viewDocument = this.editor.editing.view;
  200. const targetLink = parentLink || getPositionParentLink( viewDocument.selection.getFirstPosition() );
  201. const target = targetLink ?
  202. // When selection is inside link element, then attach panel to this element.
  203. viewDocument.domConverter.getCorrespondingDomElement( targetLink )
  204. :
  205. // Otherwise attach panel to the selection.
  206. viewDocument.domConverter.viewRangeToDom( viewDocument.selection.getFirstRange() );
  207. this.balloonPanelView.attachTo( {
  208. target,
  209. limiter: viewDocument.domConverter.getCorrespondingDomElement( viewDocument.selection.editableElement )
  210. } );
  211. }
  212. /**
  213. * Hides {@link #balloonPanelView balloon panel view}.
  214. *
  215. * @private
  216. * @param {Boolean} [focusEditable=false] When `true` then editable focus will be restored on panel hide.
  217. */
  218. _hidePanel( focusEditable ) {
  219. this.balloonPanelView.hide();
  220. if ( focusEditable ) {
  221. this.editor.editing.view.focus();
  222. }
  223. }
  224. /**
  225. * Shows {@link #balloonPanelView balloon panel view}.
  226. *
  227. * @private
  228. */
  229. _showPanel() {
  230. this._attachPanelToElement();
  231. this.formView.urlInputView.select();
  232. }
  233. }
  234. // Try to find if one of the position parent ancestors is a LinkElement,
  235. // if yes return this element.
  236. //
  237. // @private
  238. // @param {engine.view.Position} position
  239. // @returns {module:link/linkelement~LinkElement|null}
  240. function getPositionParentLink( position ) {
  241. return position.parent.getAncestors().find( ( ancestor ) => ancestor instanceof LinkElement );
  242. }