highlightstack.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. /**
  2. * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module widget/highlightstack
  7. */
  8. import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
  9. import mix from '@ckeditor/ckeditor5-utils/src/mix';
  10. /**
  11. * Class used to handle correct order of
  12. * {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#toHighlight highlights} on
  13. * elements. When different highlights are applied to same element correct order should be preserved:
  14. * * highlight with highest priority should be applied,
  15. * * if two highlights have same priority - sort by CSS class provided in
  16. * {@link module:engine/conversion/model-to-view-converters~HighlightDescriptor}.
  17. * This way, highlight will be applied with the same rules it is applied on texts.
  18. */
  19. export default class HighlightStack {
  20. /**
  21. * Creates class instance.
  22. */
  23. constructor() {
  24. this._stack = [];
  25. }
  26. /**
  27. * Adds highlight descriptor to the stack.
  28. *
  29. * @fires change:top
  30. * @param {module:engine/conversion/model-to-view-converters~HighlightDescriptor} descriptor
  31. */
  32. add( descriptor ) {
  33. const stack = this._stack;
  34. // Save top descriptor and insert new one. If top is changed - fire event.
  35. const oldTop = stack[ 0 ];
  36. this._insertDescriptor( descriptor );
  37. const newTop = stack[ 0 ];
  38. // When new object is at the top and stores different information.
  39. if ( oldTop !== newTop && !compareDescriptors( oldTop, newTop ) ) {
  40. this.fire( 'change:top', {
  41. oldDescriptor: oldTop,
  42. newDescriptor: newTop
  43. } );
  44. }
  45. }
  46. /**
  47. * Removes highlight descriptor from the stack.
  48. *
  49. * @fires change:top
  50. * @param {String} id Id of the descriptor to remove.
  51. */
  52. remove( id ) {
  53. const stack = this._stack;
  54. const oldTop = stack[ 0 ];
  55. this._removeDescriptor( id );
  56. const newTop = stack[ 0 ];
  57. // When new object is at the top and stores different information.
  58. if ( oldTop !== newTop && !compareDescriptors( oldTop, newTop ) ) {
  59. this.fire( 'change:top', {
  60. oldDescriptor: oldTop,
  61. newDescriptor: newTop
  62. } );
  63. }
  64. }
  65. /**
  66. * Inserts given descriptor in correct place in the stack. It also takes care about updating information when
  67. * descriptor with same id is already present.
  68. *
  69. * @private
  70. * @param {module:engine/conversion/model-to-view-converters~HighlightDescriptor} descriptor
  71. */
  72. _insertDescriptor( descriptor ) {
  73. const stack = this._stack;
  74. const index = stack.findIndex( item => item.id === descriptor.id );
  75. // Inserting exact same descriptor - do nothing.
  76. if ( compareDescriptors( descriptor, stack[ index ] ) ) {
  77. return;
  78. }
  79. // If descriptor with same id but with different information is on the stack - remove it.
  80. if ( index > -1 ) {
  81. stack.splice( index, 1 );
  82. }
  83. // Find correct place to insert descriptor in the stack.
  84. // It have different information (for example priority) so it must be re-inserted in correct place.
  85. let i = 0;
  86. while ( stack[ i ] && shouldABeBeforeB( stack[ i ], descriptor ) ) {
  87. i++;
  88. }
  89. stack.splice( i, 0, descriptor );
  90. }
  91. /**
  92. * Removes descriptor with given id from the stack.
  93. *
  94. * @private
  95. * @param {String} id Descriptor's id.
  96. */
  97. _removeDescriptor( id ) {
  98. const stack = this._stack;
  99. const index = stack.findIndex( item => item.id === id );
  100. // If descriptor with same id is on the list - remove it.
  101. if ( index > -1 ) {
  102. stack.splice( index, 1 );
  103. }
  104. }
  105. }
  106. mix( HighlightStack, EmitterMixin );
  107. // Compares two descriptors by checking their priority and class list.
  108. //
  109. // @param {module:engine/conversion/model-to-view-converters~HighlightDescriptor} a
  110. // @param {module:engine/conversion/model-to-view-converters~HighlightDescriptor} b
  111. // @returns {Boolean} Returns true if both descriptors are defined and have same priority and classes.
  112. function compareDescriptors( a, b ) {
  113. return a && b && a.priority == b.priority && classesToString( a.class ) == classesToString( b.class );
  114. }
  115. // Checks whenever first descriptor should be placed in the stack before second one.
  116. //
  117. // @param {module:engine/conversion/model-to-view-converters~HighlightDescriptor} a
  118. // @param {module:engine/conversion/model-to-view-converters~HighlightDescriptor} b
  119. // @returns {Boolean}
  120. function shouldABeBeforeB( a, b ) {
  121. if ( a.priority > b.priority ) {
  122. return true;
  123. } else if ( a.priority < b.priority ) {
  124. return false;
  125. }
  126. // When priorities are equal and names are different - use classes to compare.
  127. return classesToString( a.class ) > classesToString( b.class );
  128. }
  129. // Converts CSS classes passed with {@link module:engine/conversion/model-to-view-converters~HighlightDescriptor} to
  130. // sorted string.
  131. //
  132. // @param {String|Array<String>} descriptor
  133. // @returns {String}
  134. function classesToString( classes ) {
  135. return Array.isArray( classes ) ? classes.sort().join( ',' ) : classes;
  136. }
  137. /**
  138. * Fired when top element on {@link module:widget/highlightstack~HighlightStack} has been changed
  139. *
  140. * @event change:top
  141. * @param {Object} data Additional information about the change.
  142. * @param {module:engine/conversion/model-to-view-converters~HighlightDescriptor} [data.newDescriptor] New highlight
  143. * descriptor. It will be `undefined` when last descriptor is removed from the stack.
  144. * @param {module:engine/conversion/model-to-view-converters~HighlightDescriptor} [data.oldDescriptor] Old highlight
  145. * descriptor. It will be `undefined` when first descriptor is added to the stack.
  146. */