8
0

linkcommand.js 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. /**
  2. * @license Copyright (c) 2003-2020, 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. * A collection of {@link module:link/utils~ManualDecorator manual decorators}
  29. * corresponding to the {@link module:link/link~LinkConfig#decorators decorator configuration}.
  30. *
  31. * You can consider it a model with states of manual decorators added to the currently selected link.
  32. *
  33. * @readonly
  34. * @type {module:utils/collection~Collection}
  35. */
  36. this.manualDecorators = new Collection();
  37. }
  38. /**
  39. * Synchronizes the state of {@link #manualDecorators} with the currently present elements in the model.
  40. */
  41. restoreManualDecoratorStates() {
  42. for ( const manualDecorator of this.manualDecorators ) {
  43. manualDecorator.value = this._getDecoratorStateFromModel( manualDecorator.id );
  44. }
  45. }
  46. /**
  47. * @inheritDoc
  48. */
  49. refresh() {
  50. const model = this.editor.model;
  51. const doc = model.document;
  52. this.value = doc.selection.getAttribute( 'linkHref' );
  53. for ( const manualDecorator of this.manualDecorators ) {
  54. manualDecorator.value = this._getDecoratorStateFromModel( manualDecorator.id );
  55. }
  56. this.isEnabled = model.schema.checkAttributeInSelection( doc.selection, 'linkHref' );
  57. }
  58. /**
  59. * Executes the command.
  60. *
  61. * When the selection is non-collapsed, the `linkHref` attribute will be applied to nodes inside the selection, but only to
  62. * those nodes where the `linkHref` attribute is allowed (disallowed nodes will be omitted).
  63. *
  64. * When the selection is collapsed and is not inside the text with the `linkHref` attribute, a
  65. * new {@link module:engine/model/text~Text text node} with the `linkHref` attribute will be inserted in place of the caret, but
  66. * only if such element is allowed in this place. The `_data` of the inserted text will equal the `href` parameter.
  67. * The selection will be updated to wrap the just inserted text node.
  68. *
  69. * When the selection is collapsed and inside the text with the `linkHref` attribute, the attribute value will be updated.
  70. *
  71. * # Decorators and model attribute management
  72. *
  73. * There is an optional argument to this command that applies or removes model
  74. * {@glink framework/guides/architecture/editing-engine#text-attributes text attributes} brought by
  75. * {@link module:link/utils~ManualDecorator manual link decorators}.
  76. *
  77. * Text attribute names in the model correspond to the entries in the {@link module:link/link~LinkConfig#decorators configuration}.
  78. * For every decorator configured, a model text attribute exists with the "link" prefix. For example, a `'linkMyDecorator'` attribute
  79. * corresponds to `'myDecorator'` in the configuration.
  80. *
  81. * To learn more about link decorators, check out the {@link module:link/link~LinkConfig#decorators `config.link.decorators`}
  82. * documentation.
  83. *
  84. * Here is how to manage decorator attributes with the link command:
  85. *
  86. * const linkCommand = editor.commands.get( 'link' );
  87. *
  88. * // Adding a new decorator attribute.
  89. * linkCommand.execute( 'http://example.com', {
  90. * linkIsExternal: true
  91. * } );
  92. *
  93. * // Removing a decorator attribute from the selection.
  94. * linkCommand.execute( 'http://example.com', {
  95. * linkIsExternal: false
  96. * } );
  97. *
  98. * // Adding multiple decorator attributes at the same time.
  99. * linkCommand.execute( 'http://example.com', {
  100. * linkIsExternal: true,
  101. * linkIsDownloadable: true,
  102. * } );
  103. *
  104. * // Removing and adding decorator attributes at the same time.
  105. * linkCommand.execute( 'http://example.com', {
  106. * linkIsExternal: false,
  107. * linkFoo: true,
  108. * linkIsDownloadable: false,
  109. * } );
  110. *
  111. * **Note**: If the decorator attribute name is not specified, its state remains untouched.
  112. *
  113. * **Note**: {@link module:link/unlinkcommand~UnlinkCommand#execute `UnlinkCommand#execute()`} removes all
  114. * decorator attributes.
  115. *
  116. * @fires execute
  117. * @param {String} href Link destination.
  118. * @param {Object} [manualDecoratorIds={}] The information about manual decorator attributes to be applied or removed upon execution.
  119. */
  120. execute( href, manualDecoratorIds = {} ) {
  121. const model = this.editor.model;
  122. const selection = model.document.selection;
  123. // Stores information about manual decorators to turn them on/off when command is applied.
  124. const truthyManualDecorators = [];
  125. const falsyManualDecorators = [];
  126. for ( const name in manualDecoratorIds ) {
  127. if ( manualDecoratorIds[ name ] ) {
  128. truthyManualDecorators.push( name );
  129. } else {
  130. falsyManualDecorators.push( name );
  131. }
  132. }
  133. model.change( writer => {
  134. // If selection is collapsed then update selected link or insert new one at the place of caret.
  135. if ( selection.isCollapsed ) {
  136. const position = selection.getFirstPosition();
  137. // When selection is inside text with `linkHref` attribute.
  138. if ( selection.hasAttribute( 'linkHref' ) ) {
  139. // Then update `linkHref` value.
  140. const linkRange = findLinkRange( position, selection.getAttribute( 'linkHref' ), model );
  141. writer.setAttribute( 'linkHref', href, linkRange );
  142. truthyManualDecorators.forEach( item => {
  143. writer.setAttribute( item, true, linkRange );
  144. } );
  145. falsyManualDecorators.forEach( item => {
  146. writer.removeAttribute( item, linkRange );
  147. } );
  148. // Create new range wrapping changed link.
  149. writer.setSelection( linkRange );
  150. }
  151. // If not then insert text node with `linkHref` attribute in place of caret.
  152. // However, since selection in collapsed, attribute value will be used as data for text node.
  153. // So, if `href` is empty, do not create text node.
  154. else if ( href !== '' ) {
  155. const attributes = toMap( selection.getAttributes() );
  156. attributes.set( 'linkHref', href );
  157. truthyManualDecorators.forEach( item => {
  158. attributes.set( item, true );
  159. } );
  160. const node = writer.createText( href, attributes );
  161. model.insertContent( node, position );
  162. // Create new range wrapping created node.
  163. writer.setSelection( writer.createRangeOn( node ) );
  164. }
  165. } else {
  166. // If selection has non-collapsed ranges, we change attribute on nodes inside those ranges
  167. // omitting nodes where `linkHref` attribute is disallowed.
  168. const ranges = model.schema.getValidRanges( selection.getRanges(), 'linkHref' );
  169. for ( const range of ranges ) {
  170. writer.setAttribute( 'linkHref', href, range );
  171. truthyManualDecorators.forEach( item => {
  172. writer.setAttribute( item, true, range );
  173. } );
  174. falsyManualDecorators.forEach( item => {
  175. writer.removeAttribute( item, range );
  176. } );
  177. }
  178. }
  179. } );
  180. }
  181. /**
  182. * Provides information whether a decorator with a given name is present in the currently processed selection.
  183. *
  184. * @private
  185. * @param {String} decoratorName The name of the manual decorator used in the model
  186. * @returns {Boolean} The information whether a given decorator is currently present in the selection.
  187. */
  188. _getDecoratorStateFromModel( decoratorName ) {
  189. const doc = this.editor.model.document;
  190. return doc.selection.getAttribute( decoratorName );
  191. }
  192. }