headingscommand.js 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  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/command.js';
  7. import RootElement from '../engine/model/rootelement.js';
  8. export default class HeadingsCommand extends Command {
  9. constructor( editor, formats ) {
  10. super( editor );
  11. this.formats = formats;
  12. this.set( 'value', this.defaultFormat );
  13. // Listen on selection change and set current command's format to format in current selection.
  14. this.listenTo( editor.document.selection, 'change', () => {
  15. const position = editor.document.selection.getFirstPosition();
  16. const block = findTopmostBlock( position );
  17. if ( block ) {
  18. const format = this._getFormatById( block.name );
  19. // TODO: What should happen if format is not found?
  20. this.value = format;
  21. }
  22. } );
  23. }
  24. /**
  25. * The default format.
  26. *
  27. * @type {Object}
  28. */
  29. get defaultFormat() {
  30. // See https://github.com/ckeditor/ckeditor5/issues/98.
  31. return this._getFormatById( 'paragraph' );
  32. }
  33. _doExecute( formatId = this.defaultFormat.id ) {
  34. // TODO: What should happen if format is not found?
  35. const doc = this.editor.document;
  36. const selection = doc.selection;
  37. const startPosition = selection.getFirstPosition();
  38. const elements = [];
  39. // Storing selection ranges and direction to fix selection after renaming. See ckeditor5-engine#367.
  40. const ranges = [ ...selection.getRanges() ];
  41. const isSelectionBackward = selection.isBackward;
  42. // If current format is same as new format - toggle already applied format back to default one.
  43. const shouldRemove = ( formatId === this.value.id );
  44. // Collect elements to change format.
  45. // This implementation may not be future proof but it's satisfactory at this stage.
  46. if ( selection.isCollapsed ) {
  47. const block = findTopmostBlock( startPosition );
  48. if ( block ) {
  49. elements.push( block );
  50. }
  51. } else {
  52. for ( let range of ranges ) {
  53. let startBlock = findTopmostBlock( range.start );
  54. const endBlock = findTopmostBlock( range.end, false );
  55. elements.push( startBlock );
  56. while ( startBlock !== endBlock ) {
  57. startBlock = startBlock.nextSibling;
  58. elements.push( startBlock );
  59. }
  60. }
  61. }
  62. doc.enqueueChanges( () => {
  63. const batch = doc.batch();
  64. for ( let element of elements ) {
  65. // When removing applied format.
  66. if ( shouldRemove ) {
  67. if ( element.name === formatId ) {
  68. batch.rename( this.defaultFormat.id, element );
  69. }
  70. }
  71. // When applying new format.
  72. else {
  73. batch.rename( formatId, element );
  74. }
  75. }
  76. // If range's selection start/end is placed directly in renamed block - we need to restore it's position
  77. // after renaming, because renaming puts new element there.
  78. doc.selection.setRanges( ranges, isSelectionBackward );
  79. } );
  80. }
  81. /**
  82. * Returns format by given id.
  83. *
  84. * @private
  85. * @param {String} id
  86. * @returns {Object}
  87. */
  88. _getFormatById( id ) {
  89. return this.formats.find( item => item.id === id );
  90. }
  91. }
  92. // Looks for topmost element from position parent to element placed in root.
  93. //
  94. // NOTE: This method does not checks schema directly - assumes that only block elements can be placed directly inside
  95. // root.
  96. //
  97. // @private
  98. // @param {engine.model.Position} position
  99. // @param {Boolean} [nodeAfter=true] When position is placed inside root element this will determine if element before
  100. // or after given position will be returned.
  101. // @returns {engine.model.Element}
  102. function findTopmostBlock( position, nodeAfter = true ) {
  103. let parent = position.parent;
  104. // If position is placed inside root - get element after/before it.
  105. if ( parent instanceof RootElement ) {
  106. return nodeAfter ? position.nodeAfter : position.nodeBefore;
  107. }
  108. while ( !( parent.parent instanceof RootElement ) ) {
  109. parent = parent.parent;
  110. }
  111. return parent;
  112. }