linkediting.js 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. /**
  2. * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module link/linkediting
  7. */
  8. import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
  9. import {
  10. downcastAttributeToElement
  11. } from '@ckeditor/ckeditor5-engine/src/conversion/downcast-converters';
  12. import { upcastElementToAttribute } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
  13. import LinkCommand from './linkcommand';
  14. import UnlinkCommand from './unlinkcommand';
  15. import { createLinkElement } from './utils';
  16. import bindTwoStepCaretToAttribute from '@ckeditor/ckeditor5-engine/src/utils/bindtwostepcarettoattribute';
  17. import findLinkRange from './findlinkrange';
  18. import '../theme/link.css';
  19. const HIGHLIGHT_CLASSES = [ 'ck', 'ck-link_selected' ];
  20. /**
  21. * The link engine feature.
  22. *
  23. * It introduces the `linkHref="url"` attribute in the model which renders to the view as a `<a href="url">` element.
  24. *
  25. * @extends module:core/plugin~Plugin
  26. */
  27. export default class LinkEditing extends Plugin {
  28. /**
  29. * @inheritDoc
  30. */
  31. init() {
  32. const editor = this.editor;
  33. // Allow link attribute on all inline nodes.
  34. editor.model.schema.extend( '$text', { allowAttributes: 'linkHref' } );
  35. editor.conversion.for( 'downcast' )
  36. .add( downcastAttributeToElement( { model: 'linkHref', view: createLinkElement } ) );
  37. editor.conversion.for( 'upcast' )
  38. .add( upcastElementToAttribute( {
  39. view: {
  40. name: 'a',
  41. attributes: {
  42. href: true
  43. }
  44. },
  45. model: {
  46. key: 'linkHref',
  47. value: viewElement => viewElement.getAttribute( 'href' )
  48. }
  49. } ) );
  50. // Create linking commands.
  51. editor.commands.add( 'link', new LinkCommand( editor ) );
  52. editor.commands.add( 'unlink', new UnlinkCommand( editor ) );
  53. // Enable two-step caret movement for `linkHref` attribute.
  54. bindTwoStepCaretToAttribute( editor.editing.view, editor.model, this, 'linkHref' );
  55. // Setup highlight over selected link.
  56. this._setupLinkHighlight();
  57. }
  58. /**
  59. * Adds a visual highlight style to a link in which the selection is anchored.
  60. * Together with two-step caret movement, they indicate that the user is typing inside the link.
  61. *
  62. * Highlight is turned on by adding `.ck .ck-link_selected` classes to the link in the view:
  63. *
  64. * * the classes are removed before conversion has started, as callbacks added with `'highest'` priority
  65. * to {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher} events,
  66. * * the classes are added in the view post fixer, after other changes in the model tree were converted to the view.
  67. *
  68. * This way, adding and removing highlight does not interfere with conversion.
  69. *
  70. * @private
  71. */
  72. _setupLinkHighlight() {
  73. const editor = this.editor;
  74. const view = editor.editing.view;
  75. const highlightedLinks = new Set();
  76. // Adding the class.
  77. view.document.registerPostFixer( writer => {
  78. const selection = editor.model.document.selection;
  79. if ( selection.hasAttribute( 'linkHref' ) ) {
  80. const modelRange = findLinkRange( selection.getFirstPosition(), selection.getAttribute( 'linkHref' ) );
  81. const viewRange = editor.editing.mapper.toViewRange( modelRange );
  82. // There might be multiple `a` elements in the `viewRange`, for example, when the `a` element is
  83. // broken by a UIElement.
  84. for ( const item of viewRange.getItems() ) {
  85. if ( item.is( 'a' ) ) {
  86. writer.addClass( HIGHLIGHT_CLASSES, item );
  87. highlightedLinks.add( item );
  88. }
  89. }
  90. }
  91. } );
  92. // Removing the class.
  93. editor.conversion.for( 'editingDowncast' ).add( dispatcher => {
  94. // Make sure the highlight is removed on every possible event, before conversion is started.
  95. dispatcher.on( 'insert', removeHighlight, { priority: 'highest' } );
  96. dispatcher.on( 'remove', removeHighlight, { priority: 'highest' } );
  97. dispatcher.on( 'attribute', removeHighlight, { priority: 'highest' } );
  98. dispatcher.on( 'selection', removeHighlight, { priority: 'highest' } );
  99. function removeHighlight() {
  100. view.change( writer => {
  101. for ( const item of highlightedLinks.values() ) {
  102. writer.removeClass( HIGHLIGHT_CLASSES, item );
  103. highlightedLinks.delete( item );
  104. }
  105. } );
  106. }
  107. } );
  108. }
  109. }