attributecommand.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. /**
  2. * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. 'use strict';
  6. import Command from './command.js';
  7. import TreeWalker from '../core/treemodel/treewalker.js';
  8. import Range from '../core/treemodel/range.js';
  9. /**
  10. * An extension of basic {@link ckeditor5.command.Command} class, which provides utilities for a command that sets a single
  11. * attribute on a text or element with value `true`. AttributeCommand uses {@link core.treeModel.Document#selection} to
  12. * decide which nodes (if any) should be changed, and applies or removes attributes from them.
  13. * See {@link core.treeView.Converter#execute} for more.
  14. *
  15. * The command checks {@link core.treeModel.Document#schema} to decide if it should be enabled.
  16. * See {@link core.treeView.Converter#checkSchema} for more.
  17. *
  18. * @memberOf ckeditor5.command
  19. */
  20. export default class AttributeCommand extends Command {
  21. /**
  22. * @see ckeditor5.command.Command
  23. * @param {ckeditor5.Editor} editor
  24. * @param {String} attributeKey Attribute that will be set by the command.
  25. */
  26. constructor( editor, attributeKey ) {
  27. super( editor );
  28. /**
  29. * Attribute that will be set by the command.
  30. *
  31. * @member {String} ckeditor5.command.AttributeCommand#attributeKey
  32. */
  33. this.attributeKey = attributeKey;
  34. /**
  35. * Flag indicating whether command is active. For collapsed selection it means that typed characters will have
  36. * the command's attribute set. For range selection it means that all nodes inside have the attribute applied.
  37. *
  38. * @member {Boolean} ckeditor5.command.AttributeCommand#value
  39. */
  40. this.set( 'value', false );
  41. this.listenTo( this.editor.document.selection, 'change:attribute', () => {
  42. this.value = this.editor.document.selection.hasAttribute( this.attributeKey );
  43. } );
  44. }
  45. /**
  46. * Checks {@link core.treeModel.Document#schema} to decide if the command should be enabled:
  47. * * if selection is on range, the command is enabled if any of nodes in that range can have bold,
  48. * * if selection is collapsed, the command is enabled if text with bold is allowed in that node.
  49. *
  50. * @private
  51. * @returns {Boolean}
  52. */
  53. _checkEnabled() {
  54. const selection = this.editor.document.selection;
  55. const schema = this.editor.document.schema;
  56. if ( selection.isCollapsed ) {
  57. // Check whether schema allows for a test with `attributeKey` in caret position.
  58. return schema.checkAtPosition( selection.getFirstPosition(), '$text', this.attributeKey );
  59. } else {
  60. const ranges = selection.getRanges();
  61. // For all ranges, check nodes in them until you find a node that is allowed to have `attributeKey` attribute.
  62. for ( let range of ranges ) {
  63. const walker = new TreeWalker( { boundaries: range, mergeCharacters: true } );
  64. let last = walker.position;
  65. let step = walker.next();
  66. // Walk the range.
  67. while ( !step.done ) {
  68. // If returned item does not have name property, it is a treeModel.TextFragment.
  69. const name = step.value.item.name || '$text';
  70. if ( schema.checkAtPosition( last, name, this.attributeKey ) ) {
  71. // If we found a node that is allowed to have the attribute, return true.
  72. return true;
  73. }
  74. last = walker.position;
  75. step = walker.next();
  76. }
  77. }
  78. }
  79. // If we haven't found such node, return false.
  80. return false;
  81. }
  82. /**
  83. * Executes the command: adds or removes attributes to nodes or selection.
  84. *
  85. * If the command is active (`value == true`), it will remove attributes. Otherwise, it will set attributes.
  86. *
  87. * The execution result differs, depending on the {@link core.treeModel.Document#selection}:
  88. * * if selection is on a range, the command applies the attribute on all nodes in that ranges
  89. * (if they are allowed to have this attribute by the{@link core.treeModel.Schema schema}),
  90. * * if selection is collapsed in non-empty node, the command applies attribute to the {@link core.treeModel.Document#selection}
  91. * itself (note that typed characters copy attributes from selection),
  92. * * if selection is collapsed in empty node, the command applies attribute to the parent node of selection (note
  93. * that selection inherits all attributes from a node if it is in empty node).
  94. *
  95. * If the command is disabled (`isEnabled == false`) when it is executed, nothing will happen.
  96. *
  97. * @private
  98. * @param {Boolean} [forceValue] If set it will force command behavior. If `true`, command will apply attribute,
  99. * otherwise command will remove attribute. If not set, command will look for it's current value to decide what it should do.
  100. */
  101. _doExecute( forceValue ) {
  102. const document = this.editor.document;
  103. const selection = document.selection;
  104. const value = ( forceValue === undefined ) ? !this.value : forceValue;
  105. if ( selection.isCollapsed ) {
  106. if ( value ) {
  107. selection.setAttribute( this.attributeKey, true );
  108. } else {
  109. selection.removeAttribute( this.attributeKey );
  110. }
  111. } else {
  112. // If selection has non-collapsed ranges, we change attribute on nodes inside those ranges.
  113. document.enqueueChanges( () => {
  114. const ranges = this._getSchemaValidRanges( selection.getRanges() );
  115. // Keep it as one undo step.
  116. const batch = document.batch();
  117. for ( let range of ranges ) {
  118. if ( value ) {
  119. batch.setAttr( this.attributeKey, value, range );
  120. } else {
  121. batch.removeAttr( this.attributeKey, range );
  122. }
  123. }
  124. } );
  125. }
  126. }
  127. /**
  128. * Walks through given array of ranges and removes parts of them that are not allowed by schema to have the
  129. * attribute set. This is done by breaking a range in two and omitting the not allowed part.
  130. *
  131. * @private
  132. * @param {Array.<core.treeModel.Range>} ranges Ranges to be validated.
  133. * @returns {Array.<core.treeModel.Range>} Ranges without invalid parts.
  134. */
  135. _getSchemaValidRanges( ranges ) {
  136. const validRanges = [];
  137. for ( let range of ranges ) {
  138. const walker = new TreeWalker( { boundaries: range, mergeCharacters: true } );
  139. let step = walker.next();
  140. let last = range.start;
  141. let from = range.start;
  142. let to = range.end;
  143. while ( !step.done ) {
  144. const name = step.value.item.name || '$text';
  145. if ( !this.editor.document.schema.checkAtPosition( last, name, this.attributeKey ) ) {
  146. if ( !from.isEqual( last ) ) {
  147. validRanges.push( new Range( from, last ) );
  148. }
  149. from = walker.position;
  150. }
  151. last = walker.position;
  152. step = walker.next();
  153. }
  154. if ( from && !from.isEqual( to ) ) {
  155. validRanges.push( new Range( from, to ) );
  156. }
  157. }
  158. return validRanges;
  159. }
  160. }