linkediting.js 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  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 bindTwoStepCaretToAttribute from '@ckeditor/ckeditor5-engine/src/utils/bindtwostepcarettoattribute';
  14. import findLinkRange from './findlinkrange';
  15. import '../theme/link.css';
  16. const HIGHLIGHT_CLASS = 'ck-link_selected';
  17. const AUTO = 'automatic';
  18. /**
  19. * The link engine feature.
  20. *
  21. * It introduces the `linkHref="url"` attribute in the model which renders to the view as a `<a href="url">` element
  22. * as well as `'link'` and `'unlink'` commands.
  23. *
  24. * @extends module:core/plugin~Plugin
  25. */
  26. export default class LinkEditing extends Plugin {
  27. /**
  28. * @inheritDoc
  29. */
  30. constructor( editor ) {
  31. super( editor );
  32. editor.config.define( 'link', {
  33. targetDecorator: false
  34. } );
  35. }
  36. /**
  37. * @inheritDoc
  38. */
  39. init() {
  40. const editor = this.editor;
  41. // Allow link attribute on all inline nodes.
  42. editor.model.schema.extend( '$text', { allowAttributes: 'linkHref' } );
  43. editor.conversion.for( 'dataDowncast' )
  44. .attributeToElement( { model: 'linkHref', view: createLinkElement } );
  45. editor.conversion.for( 'editingDowncast' )
  46. .attributeToElement( { model: 'linkHref', view: ( href, writer ) => {
  47. return createLinkElement( ensureSafeUrl( href ), writer );
  48. } } );
  49. editor.conversion.for( 'upcast' )
  50. .elementToAttribute( {
  51. view: {
  52. name: 'a',
  53. attributes: {
  54. href: true
  55. }
  56. },
  57. model: {
  58. key: 'linkHref',
  59. value: viewElement => viewElement.getAttribute( 'href' )
  60. }
  61. } );
  62. const automaticDecorators = new AutomaticDecorators();
  63. if ( editor.config.get( 'link.targetDecorator' ) ) {
  64. automaticDecorators.add( {
  65. mode: AUTO,
  66. callback: url => {
  67. const EXTERNAL_LINKS_REGEXP = /^(https?:)?\/\//;
  68. return EXTERNAL_LINKS_REGEXP.test( url );
  69. },
  70. attributes: {
  71. target: '_blank',
  72. rel: 'noopener noreferrer'
  73. }
  74. } );
  75. }
  76. const linkDecorators = editor.config.get( 'link.decorators' ) || [];
  77. automaticDecorators.add( linkDecorators.filter( item => item.mode === AUTO ) );
  78. editor.conversion.for( 'downcast' ).add( automaticDecorators.getDispatcher() );
  79. // Create linking commands.
  80. editor.commands.add( 'link', new LinkCommand( editor ) );
  81. editor.commands.add( 'unlink', new UnlinkCommand( editor ) );
  82. // Enable two-step caret movement for `linkHref` attribute.
  83. bindTwoStepCaretToAttribute( editor.editing.view, editor.model, this, 'linkHref' );
  84. // Setup highlight over selected link.
  85. this._setupLinkHighlight();
  86. }
  87. /**
  88. * Adds a visual highlight style to a link in which the selection is anchored.
  89. * Together with two-step caret movement, they indicate that the user is typing inside the link.
  90. *
  91. * Highlight is turned on by adding `.ck-link_selected` class to the link in the view:
  92. *
  93. * * the class is removed before conversion has started, as callbacks added with `'highest'` priority
  94. * to {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher} events,
  95. * * the class is added in the view post fixer, after other changes in the model tree were converted to the view.
  96. *
  97. * This way, adding and removing highlight does not interfere with conversion.
  98. *
  99. * @private
  100. */
  101. _setupLinkHighlight() {
  102. const editor = this.editor;
  103. const view = editor.editing.view;
  104. const highlightedLinks = new Set();
  105. // Adding the class.
  106. view.document.registerPostFixer( writer => {
  107. const selection = editor.model.document.selection;
  108. if ( selection.hasAttribute( 'linkHref' ) ) {
  109. const modelRange = findLinkRange( selection.getFirstPosition(), selection.getAttribute( 'linkHref' ), editor.model );
  110. const viewRange = editor.editing.mapper.toViewRange( modelRange );
  111. // There might be multiple `a` elements in the `viewRange`, for example, when the `a` element is
  112. // broken by a UIElement.
  113. for ( const item of viewRange.getItems() ) {
  114. if ( item.is( 'a' ) ) {
  115. writer.addClass( HIGHLIGHT_CLASS, item );
  116. highlightedLinks.add( item );
  117. }
  118. }
  119. }
  120. } );
  121. // Removing the class.
  122. editor.conversion.for( 'editingDowncast' ).add( dispatcher => {
  123. // Make sure the highlight is removed on every possible event, before conversion is started.
  124. dispatcher.on( 'insert', removeHighlight, { priority: 'highest' } );
  125. dispatcher.on( 'remove', removeHighlight, { priority: 'highest' } );
  126. dispatcher.on( 'attribute', removeHighlight, { priority: 'highest' } );
  127. dispatcher.on( 'selection', removeHighlight, { priority: 'highest' } );
  128. function removeHighlight() {
  129. view.change( writer => {
  130. for ( const item of highlightedLinks.values() ) {
  131. writer.removeClass( HIGHLIGHT_CLASS, item );
  132. highlightedLinks.delete( item );
  133. }
  134. } );
  135. }
  136. } );
  137. }
  138. }