link.js 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. /**
  2. * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. import Feature from '../core/feature.js';
  6. import ClickObserver from '../engine/view/observer/clickobserver.js';
  7. import LinkEngine from './linkengine.js';
  8. import LinkElement from './linkelement.js';
  9. import Model from '../ui/model.js';
  10. import ButtonController from '../ui/button/button.js';
  11. import ButtonView from '../ui/button/buttonview.js';
  12. import LinkBalloonPanel from './ui/linkballoonpanel.js';
  13. import LinkBalloonPanelView from './ui/linkballoonpanelview.js';
  14. /**
  15. * The link feature.
  16. *
  17. * It uses the {@link basic-styles.LinkEngine link engine feature}.
  18. *
  19. * @memberOf link
  20. * @extends core.Feature
  21. */
  22. export default class Link extends Feature {
  23. /**
  24. * @inheritDoc
  25. */
  26. static get requires() {
  27. return [ LinkEngine ];
  28. }
  29. /**
  30. * @inheritDoc
  31. */
  32. init() {
  33. const viewDocument = this.editor.editing.view;
  34. // Register document click observer.
  35. viewDocument.addObserver( ClickObserver );
  36. /**
  37. * Link balloon panel component.
  38. *
  39. * @member {link.ui.LinkBalloonPanel}
  40. */
  41. this.balloonPanel = this._createBalloonPanel();
  42. // Create toolbar buttons.
  43. this._createToolbarLinkButton();
  44. this._createToolbarUnlinkButton();
  45. }
  46. /**
  47. * Create toolbar link button. Click on button will show
  48. * {@link link.ui.LinkBalloonPanel LinkBalloonPanel} attached to the selection.
  49. *
  50. * @private
  51. */
  52. _createToolbarLinkButton() {
  53. const editor = this.editor;
  54. const viewDocument = editor.editing.view;
  55. const linkCommand = editor.commands.get( 'link' );
  56. const t = editor.t;
  57. // Create button model.
  58. const linkButtonModel = new Model( {
  59. isEnabled: true,
  60. isOn: false,
  61. label: t( 'Link' ),
  62. icon: 'link'
  63. } );
  64. // Bind button model to command.
  65. linkButtonModel.bind( 'isEnabled' ).to( linkCommand, 'isEnabled' );
  66. // Show Balloon Panel on button click only when editor is focused.
  67. this.listenTo( linkButtonModel, 'execute', () => {
  68. if ( !viewDocument.isFocused ) {
  69. return;
  70. }
  71. this._attachPanelToElement();
  72. } );
  73. // Add link button to feature components.
  74. editor.ui.featureComponents.add( 'link', ButtonController, ButtonView, linkButtonModel );
  75. }
  76. /**
  77. * Create toolbar unlink button. Click on button will unlink selected link.
  78. *
  79. * @private
  80. */
  81. _createToolbarUnlinkButton() {
  82. const editor = this.editor;
  83. const t = editor.t;
  84. const unlinkCommand = editor.commands.get( 'unlink' );
  85. // Create button model.
  86. const unlinkButtonModel = new Model( {
  87. isEnabled: false,
  88. isOn: false,
  89. label: t( 'Unlink' ),
  90. icon: 'unlink'
  91. } );
  92. // Bind button model to command.
  93. unlinkButtonModel.bind( 'isEnabled' ).to( unlinkCommand, 'hasValue' );
  94. // Execute unlink command and hide panel if is opened.
  95. this.listenTo( unlinkButtonModel, 'execute', () => {
  96. editor.execute( 'unlink' );
  97. if ( this.balloonPanel && this.balloonPanel.view.isVisible ) {
  98. this.balloonPanel.view.hide();
  99. }
  100. } );
  101. // Add unlink button to feature components.
  102. editor.ui.featureComponents.add( 'unlink', ButtonController, ButtonView, unlinkButtonModel );
  103. }
  104. /**
  105. * Create {@link link.ui.LinkBalloonPanel LinkBalloonPanel} instance
  106. * and attach link command to LinkBalloonPanelModel#execute event.
  107. *
  108. * +------------------------------------+
  109. * | <a href="http://foo.com">[foo]</a> |
  110. * +------------------------------------+
  111. * Document
  112. * Value set in doc ^ +
  113. * if it's correct. | |
  114. * | |
  115. * +---------+--------+ |
  116. * Panel.urlInput#value | Value validation | | User clicked "Link" in
  117. * is validated. +---------+--------+ | the toolbar. Retrieving
  118. * | | URL from Document and setting
  119. * PanelModel fires | | PanelModel#url.
  120. * PanelModel#execute. + v
  121. *
  122. * +-----------------------+
  123. * | url: 'http://foo.com' |
  124. * +-----------------------+
  125. * PanelModel
  126. * ^ +
  127. * | | Input field is
  128. * User clicked | | in sync with
  129. * "Save". | | PanelModel#url.
  130. * + v
  131. *
  132. * +--------------------------+
  133. * | +----------------------+ |
  134. * | |http://foo.com | |
  135. * | +----------------------+ |
  136. * | +----+ |
  137. * | |Save| |
  138. * | +----+ |
  139. * +--------------------------+
  140. * @private
  141. * @returns {link.ui.LinkBalloonPanel} Link balloon panel instance.
  142. */
  143. _createBalloonPanel() {
  144. const editor = this.editor;
  145. const viewDocument = editor.editing.view;
  146. const linkCommand = editor.commands.get( 'link' );
  147. // Create the model of the panel.
  148. const panelModel = new Model( {
  149. maxWidth: 300,
  150. url: linkCommand.value
  151. } );
  152. // Bind panel model to command.
  153. panelModel.bind( 'url' ).to( linkCommand, 'value' );
  154. // Create Balloon panel instance.
  155. const balloonPanel = new LinkBalloonPanel( panelModel, new LinkBalloonPanelView( editor.locale ) );
  156. // Observe `LinkBalloonPanelMode#execute` event from within the model of the panel,
  157. // which means that the `Save` button has been clicked.
  158. this.listenTo( panelModel, 'execute', () => {
  159. editor.execute( 'link', balloonPanel.urlInput.value );
  160. balloonPanel.view.hide();
  161. } );
  162. // Observe `LinkBalloonPanelMode#execute-unlink` event from within the model of the panel,
  163. // which means that the `Unlink` button has been clicked.
  164. this.listenTo( panelModel, 'execute-unlink', () => {
  165. editor.execute( 'unlink' );
  166. balloonPanel.view.hide();
  167. } );
  168. // Always focus editor on panel hide.
  169. this.listenTo( panelModel, 'hide', () => viewDocument.focus() );
  170. // Hide panel on editor focus.
  171. // @TODO replace it by some FocusManager.
  172. viewDocument.on( 'focus', () => balloonPanel.view.hide() );
  173. // Handle click on document and show panel when selection is placed in the link element.
  174. viewDocument.on( 'click', () => {
  175. if ( viewDocument.selection.isCollapsed && linkCommand.value !== undefined ) {
  176. this._attachPanelToElement();
  177. }
  178. } );
  179. // Handle `Ctrl+l` keystroke and show panel.
  180. editor.keystrokes.set( 'CTRL+l', () => this._attachPanelToElement() );
  181. // Append panel element to body.
  182. editor.ui.add( 'body', balloonPanel );
  183. return balloonPanel;
  184. }
  185. /**
  186. * Show {@link link.ui.LinkBalloonPanel LinkBalloonPanel} and attach to target element.
  187. * If selection is collapsed and is placed inside link element, then panel will be attached
  188. * to whole link element, otherwise will be attached to the selection.
  189. *
  190. * Input inside panel will be focused.
  191. *
  192. * @private
  193. */
  194. _attachPanelToElement() {
  195. const viewDocument = this.editor.editing.view;
  196. const domEditableElement = viewDocument.domConverter.getCorrespondingDomElement( viewDocument.selection.editableElement );
  197. const viewSelectionParent = viewDocument.selection.getFirstPosition().parent;
  198. const viewSelectionParentAncestors = viewSelectionParent.getAncestors();
  199. const linkElement = viewSelectionParentAncestors.find( ( ancestor ) => ancestor instanceof LinkElement );
  200. // When selection is inside link element, then attach panel to this element.
  201. if ( linkElement ) {
  202. this.balloonPanel.view.attachTo(
  203. viewDocument.domConverter.getCorrespondingDomElement( linkElement ),
  204. domEditableElement
  205. );
  206. }
  207. // Otherwise attach panel to the selection.
  208. else {
  209. this.balloonPanel.view.attachTo(
  210. viewDocument.domConverter.viewRangeToDom( viewDocument.selection.getFirstRange() ),
  211. domEditableElement
  212. );
  213. }
  214. // Set focus to the panel input.
  215. this.balloonPanel.urlInput.view.focus();
  216. }
  217. }