linkediting.js 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. /**
  2. * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  4. */
  5. /**
  6. * @module link/linkediting
  7. */
  8. import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
  9. import LinkCommand from './linkcommand';
  10. import UnlinkCommand from './unlinkcommand';
  11. import { createLinkElement, ensureSafeUrl } from './utils';
  12. import AutomaticDecorators from './utils/automaticdecorators';
  13. import ManualDecorator from './utils/manualdecorator';
  14. import bindTwoStepCaretToAttribute from '@ckeditor/ckeditor5-engine/src/utils/bindtwostepcarettoattribute';
  15. import findLinkRange from './findlinkrange';
  16. import '../theme/link.css';
  17. const HIGHLIGHT_CLASS = 'ck-link_selected';
  18. const DECORATOR_AUTOMATIC = 'automatic';
  19. const DECORATOR_MANUAL = 'manual';
  20. const EXTERNAL_LINKS_REGEXP = /^(https?:)?\/\//;
  21. /**
  22. * The link engine feature.
  23. *
  24. * It introduces the `linkHref="url"` attribute in the model which renders to the view as a `<a href="url">` element
  25. * as well as `'link'` and `'unlink'` commands.
  26. *
  27. * @extends module:core/plugin~Plugin
  28. */
  29. export default class LinkEditing extends Plugin {
  30. /**
  31. * @inheritDoc
  32. */
  33. constructor( editor ) {
  34. super( editor );
  35. editor.config.define( 'link', {
  36. addTargetToExternalLinks: false
  37. } );
  38. }
  39. /**
  40. * @inheritDoc
  41. */
  42. init() {
  43. const editor = this.editor;
  44. // Allow link attribute on all inline nodes.
  45. editor.model.schema.extend( '$text', { allowAttributes: 'linkHref' } );
  46. editor.conversion.for( 'dataDowncast' )
  47. .attributeToElement( { model: 'linkHref', view: createLinkElement } );
  48. editor.conversion.for( 'editingDowncast' )
  49. .attributeToElement( { model: 'linkHref', view: ( href, writer ) => {
  50. return createLinkElement( ensureSafeUrl( href ), writer );
  51. } } );
  52. editor.conversion.for( 'upcast' )
  53. .elementToAttribute( {
  54. view: {
  55. name: 'a',
  56. attributes: {
  57. href: true
  58. }
  59. },
  60. model: {
  61. key: 'linkHref',
  62. value: viewElement => viewElement.getAttribute( 'href' )
  63. }
  64. } );
  65. // Create linking commands.
  66. editor.commands.add( 'link', new LinkCommand( editor ) );
  67. editor.commands.add( 'unlink', new UnlinkCommand( editor ) );
  68. const linkDecorators = editor.config.get( 'link.decorators' ) || [];
  69. this._enableAutomaticDecorators( linkDecorators.filter( item => item.mode === DECORATOR_AUTOMATIC ) );
  70. this._enableManualDecorators( linkDecorators.filter( item => item.mode === DECORATOR_MANUAL ) );
  71. // Enable two-step caret movement for `linkHref` attribute.
  72. bindTwoStepCaretToAttribute( editor.editing.view, editor.model, this, 'linkHref' );
  73. // Setup highlight over selected link.
  74. this._setupLinkHighlight();
  75. }
  76. /**
  77. * Processes an array of configured {@link module:link/link~LinkDecoratorAutomaticDefinition automatic decorators}
  78. * and registers a {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher downcast dispatcher}
  79. * for each one of them. Downcast dispatchers are obtained using the
  80. * {@link module:link/utils~AutomaticDecorators#getDispatcher} method.
  81. *
  82. * **Note**: This method also activates the automatic external link decorator if enabled via
  83. * {@link module:link/link~LinkConfig#addTargetToExternalLinks `config.link.addTargetToExternalLinks`}.
  84. *
  85. * @private
  86. * @param {Array.<module:link/link~LinkDecoratorAutomaticDefinition>} automaticDecoratorDefinitions
  87. */
  88. _enableAutomaticDecorators( automaticDecoratorDefinitions ) {
  89. const editor = this.editor;
  90. const automaticDecorators = new AutomaticDecorators();
  91. // Adds default decorator for external links.
  92. if ( editor.config.get( 'link.addTargetToExternalLinks' ) ) {
  93. automaticDecorators.add( {
  94. mode: DECORATOR_AUTOMATIC,
  95. callback: url => EXTERNAL_LINKS_REGEXP.test( url ),
  96. attributes: {
  97. target: '_blank',
  98. rel: 'noopener noreferrer'
  99. }
  100. } );
  101. }
  102. automaticDecorators.add( automaticDecoratorDefinitions );
  103. if ( automaticDecorators.length ) {
  104. editor.conversion.for( 'downcast' ).add( automaticDecorators.getDispatcher() );
  105. }
  106. }
  107. /**
  108. * Processes an array of configured {@link module:link/link~LinkDecoratorManualDefinition manual decorators}
  109. * and transforms them into {@link module:link/utils~ManualDecorator} instances and stores them in the
  110. * {@link module:link/linkcommand~LinkCommand#manualDecorators} collection (a model for manual decorators state).
  111. *
  112. * Also registers an {@link module:engine/conversion/downcasthelpers~DowncastHelpers#attributeToElement attributeToElement}
  113. * converter for each manual decorator and extends the {@link module:engine/model/schema~Schema model's schema}
  114. * with adequate model attributes.
  115. *
  116. * @private
  117. * @param {Array.<module:link/link~LinkDecoratorManualDefinition>} manualDecoratorDefinitions
  118. */
  119. _enableManualDecorators( manualDecoratorDefinitions ) {
  120. if ( !manualDecoratorDefinitions.length ) {
  121. return;
  122. }
  123. const editor = this.editor;
  124. const command = editor.commands.get( 'link' );
  125. const manualDecorators = command.manualDecorators;
  126. manualDecoratorDefinitions.forEach( ( decorator, index ) => {
  127. const decoratorName = `linkManualDecorator${ index }`;
  128. editor.model.schema.extend( '$text', { allowAttributes: decoratorName } );
  129. // Keeps reference to manual decorator to decode its name to attributes during downcast.
  130. manualDecorators.add( new ManualDecorator( Object.assign( { id: decoratorName }, decorator ) ) );
  131. editor.conversion.for( 'downcast' ).attributeToElement( {
  132. model: decoratorName,
  133. view: ( manualDecoratorName, writer ) => {
  134. if ( manualDecoratorName ) {
  135. const element = writer.createAttributeElement(
  136. 'a',
  137. manualDecorators.get( decoratorName ).attributes,
  138. {
  139. priority: 5
  140. }
  141. );
  142. writer.setCustomProperty( 'link', true, element );
  143. return element;
  144. }
  145. } } );
  146. } );
  147. }
  148. /**
  149. * Adds a visual highlight style to a link in which the selection is anchored.
  150. * Together with two-step caret movement, they indicate that the user is typing inside the link.
  151. *
  152. * Highlight is turned on by adding `.ck-link_selected` class to the link in the view:
  153. *
  154. * * the class is removed before conversion has started, as callbacks added with `'highest'` priority
  155. * to {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher} events,
  156. * * the class is added in the view post fixer, after other changes in the model tree were converted to the view.
  157. *
  158. * This way, adding and removing highlight does not interfere with conversion.
  159. *
  160. * @private
  161. */
  162. _setupLinkHighlight() {
  163. const editor = this.editor;
  164. const view = editor.editing.view;
  165. const highlightedLinks = new Set();
  166. // Adding the class.
  167. view.document.registerPostFixer( writer => {
  168. const selection = editor.model.document.selection;
  169. if ( selection.hasAttribute( 'linkHref' ) ) {
  170. const modelRange = findLinkRange( selection.getFirstPosition(), selection.getAttribute( 'linkHref' ), editor.model );
  171. const viewRange = editor.editing.mapper.toViewRange( modelRange );
  172. // There might be multiple `a` elements in the `viewRange`, for example, when the `a` element is
  173. // broken by a UIElement.
  174. for ( const item of viewRange.getItems() ) {
  175. if ( item.is( 'a' ) ) {
  176. writer.addClass( HIGHLIGHT_CLASS, item );
  177. highlightedLinks.add( item );
  178. }
  179. }
  180. }
  181. } );
  182. // Removing the class.
  183. editor.conversion.for( 'editingDowncast' ).add( dispatcher => {
  184. // Make sure the highlight is removed on every possible event, before conversion is started.
  185. dispatcher.on( 'insert', removeHighlight, { priority: 'highest' } );
  186. dispatcher.on( 'remove', removeHighlight, { priority: 'highest' } );
  187. dispatcher.on( 'attribute', removeHighlight, { priority: 'highest' } );
  188. dispatcher.on( 'selection', removeHighlight, { priority: 'highest' } );
  189. function removeHighlight() {
  190. view.change( writer => {
  191. for ( const item of highlightedLinks.values() ) {
  192. writer.removeClass( HIGHLIGHT_CLASS, item );
  193. highlightedLinks.delete( item );
  194. }
  195. } );
  196. }
  197. } );
  198. }
  199. }