8
0

linkcommand.js 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  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/linkcommand
  7. */
  8. import Command from '@ckeditor/ckeditor5-core/src/command';
  9. import findLinkRange from './findlinkrange';
  10. import toMap from '@ckeditor/ckeditor5-utils/src/tomap';
  11. import Collection from '@ckeditor/ckeditor5-utils/src/collection';
  12. /**
  13. * The link command. It is used by the {@link module:link/link~Link link feature}.
  14. *
  15. * @extends module:core/command~Command
  16. */
  17. export default class LinkCommand extends Command {
  18. /**
  19. * The value of the `'linkHref'` attribute if the start of the selection is located in a node with this attribute.
  20. *
  21. * @observable
  22. * @readonly
  23. * @member {Object|undefined} #value
  24. */
  25. constructor( editor ) {
  26. super( editor );
  27. /**
  28. * Keeps collection of {@link module:link/utils~ManualDecorator}
  29. * corresponding to {@link module:link/link~LinkConfig#decorators}.
  30. * You can consider it as a model of states for custom attributes added to links.
  31. *
  32. * @readonly
  33. * @type {module:utils/collection~Collection}
  34. */
  35. this.customAttributes = new Collection();
  36. }
  37. /**
  38. * @inheritDoc
  39. */
  40. refresh() {
  41. const model = this.editor.model;
  42. const doc = model.document;
  43. this.value = doc.selection.getAttribute( 'linkHref' );
  44. for ( const customAttr of this.customAttributes ) {
  45. customAttr.value = doc.selection.getAttribute( customAttr.id ) || false;
  46. }
  47. this.isEnabled = model.schema.checkAttributeInSelection( doc.selection, 'linkHref' );
  48. }
  49. /**
  50. * Executes the command.
  51. *
  52. * When the selection is non-collapsed, the `linkHref` attribute will be applied to nodes inside the selection, but only to
  53. * those nodes where the `linkHref` attribute is allowed (disallowed nodes will be omitted).
  54. *
  55. * When the selection is collapsed and is not inside the text with the `linkHref` attribute, the
  56. * new {@link module:engine/model/text~Text Text node} with the `linkHref` attribute will be inserted in place of caret, but
  57. * only if such element is allowed in this place. The `_data` of the inserted text will equal the `href` parameter.
  58. * The selection will be updated to wrap the just inserted text node.
  59. *
  60. * When the selection is collapsed and inside the text with the `linkHref` attribute, the attribute value will be updated.
  61. *
  62. * @fires execute
  63. * @param {String} href Link destination.
  64. */
  65. execute( href, customAttrs = {} ) {
  66. const model = this.editor.model;
  67. const selection = model.document.selection;
  68. // Stores information about custom attributes to turn on/off.
  69. const truthyCustomAttributes = [];
  70. const falsyCustomAttributes = [];
  71. for ( const name in customAttrs ) {
  72. if ( customAttrs[ name ] ) {
  73. truthyCustomAttributes.push( name );
  74. } else {
  75. falsyCustomAttributes.push( name );
  76. }
  77. }
  78. model.change( writer => {
  79. // If selection is collapsed then update selected link or insert new one at the place of caret.
  80. if ( selection.isCollapsed ) {
  81. const position = selection.getFirstPosition();
  82. // When selection is inside text with `linkHref` attribute.
  83. if ( selection.hasAttribute( 'linkHref' ) ) {
  84. // Then update `linkHref` value.
  85. const linkRange = findLinkRange( position, selection.getAttribute( 'linkHref' ), model );
  86. writer.setAttribute( 'linkHref', href, linkRange );
  87. truthyCustomAttributes.forEach( item => {
  88. writer.setAttribute( item, true, linkRange );
  89. } );
  90. falsyCustomAttributes.forEach( item => {
  91. writer.removeAttribute( item, linkRange );
  92. } );
  93. // Create new range wrapping changed link.
  94. writer.setSelection( linkRange );
  95. }
  96. // If not then insert text node with `linkHref` attribute in place of caret.
  97. // However, since selection in collapsed, attribute value will be used as data for text node.
  98. // So, if `href` is empty, do not create text node.
  99. else if ( href !== '' ) {
  100. const attributes = toMap( selection.getAttributes() );
  101. attributes.set( 'linkHref', href );
  102. truthyCustomAttributes.forEach( item => {
  103. attributes.set( item, true );
  104. } );
  105. const node = writer.createText( href, attributes );
  106. model.insertContent( node, position );
  107. // Create new range wrapping created node.
  108. writer.setSelection( writer.createRangeOn( node ) );
  109. }
  110. } else {
  111. // If selection has non-collapsed ranges, we change attribute on nodes inside those ranges
  112. // omitting nodes where `linkHref` attribute is disallowed.
  113. const ranges = model.schema.getValidRanges( selection.getRanges(), 'linkHref' );
  114. for ( const range of ranges ) {
  115. writer.setAttribute( 'linkHref', href, range );
  116. truthyCustomAttributes.forEach( item => {
  117. writer.setAttribute( item, true, range );
  118. } );
  119. falsyCustomAttributes.forEach( item => {
  120. writer.removeAttribute( item, range );
  121. } );
  122. }
  123. }
  124. } );
  125. }
  126. }