headingengine.js 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. /**
  2. * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module heading/headingengine
  7. */
  8. import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
  9. import Collection from '@ckeditor/ckeditor5-utils/src/collection';
  10. import buildModelConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildmodelconverter';
  11. import buildViewConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildviewconverter';
  12. import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
  13. import HeadingCommand from './headingcommand';
  14. const defaultOptionId = 'paragraph';
  15. /**
  16. * The headings engine feature. It handles switching between block formats – headings and paragraph.
  17. * This class represents the engine part of the heading feature. See also {@link module:heading/heading~Heading}.
  18. *
  19. * @extends modules:core/plugin~Plugin
  20. */
  21. export default class HeadingEngine extends Plugin {
  22. /**
  23. * @inheritDoc
  24. */
  25. constructor( editor ) {
  26. super( editor );
  27. /**
  28. * A collection of heading commands associated with heading engine.
  29. *
  30. * @readonly
  31. * @member {module:utils/collection~Collection.<module:heading/headingcommand~HeadingCommand>}
  32. */
  33. this.commands = new Collection();
  34. editor.config.define( 'heading', {
  35. options: [
  36. { id: 'paragraph', element: 'p', label: 'Paragraph' },
  37. { id: 'heading1', element: 'h2', label: 'Heading 1' },
  38. { id: 'heading2', element: 'h3', label: 'Heading 2' },
  39. { id: 'heading3', element: 'h4', label: 'Heading 3' }
  40. ]
  41. } );
  42. }
  43. /**
  44. * @inheritDoc
  45. */
  46. static get requires() {
  47. return [ Paragraph ];
  48. }
  49. /**
  50. * @inheritDoc
  51. */
  52. init() {
  53. const editor = this.editor;
  54. const data = editor.data;
  55. const editing = editor.editing;
  56. const options = this._getLocalizedOptions();
  57. for ( let option of options ) {
  58. // Skip paragraph - it is defined in required Paragraph feature.
  59. if ( option.id !== defaultOptionId ) {
  60. // Schema.
  61. editor.document.schema.registerItem( option.id, '$block' );
  62. // Build converter from model to view for data and editing pipelines.
  63. buildModelConverter().for( data.modelToView, editing.modelToView )
  64. .fromElement( option.id )
  65. .toElement( option.element );
  66. // Build converter from view to model for data pipeline.
  67. buildViewConverter().for( data.viewToModel )
  68. .fromElement( option.element )
  69. .toElement( option.id );
  70. }
  71. // Register the heading command for this option.
  72. const command = new HeadingCommand( editor, option );
  73. this.commands.add( command );
  74. editor.commands.set( command.name, command );
  75. }
  76. }
  77. /**
  78. * @inheritDoc
  79. */
  80. afterInit() {
  81. // If the enter command is added to the editor, alter its behavior.
  82. // Enter at the end of a heading element should create a paragraph.
  83. const editor = this.editor;
  84. const enterCommand = editor.commands.get( 'enter' );
  85. const options = this._getLocalizedOptions();
  86. if ( enterCommand ) {
  87. this.listenTo( enterCommand, 'afterExecute', ( evt, data ) => {
  88. const positionParent = editor.document.selection.getFirstPosition().parent;
  89. const batch = data.batch;
  90. const isHeading = options.some( option => option.id == positionParent.name );
  91. if ( isHeading && positionParent.name != defaultOptionId && positionParent.childCount === 0 ) {
  92. batch.rename( positionParent, defaultOptionId );
  93. }
  94. } );
  95. }
  96. }
  97. /**
  98. * Returns heading options as defined in `config.heading.options` but processed to consider
  99. * editor localization, i.e. to display {@link module:heading/headingcommand~HeadingOption#label}
  100. * in the correct language.
  101. *
  102. * Note: The reason behind this method is that there's no way to use {@link utils/locale~Locale#t}
  103. * when the user config is defined because the editor does not exist yet.
  104. *
  105. * @private
  106. * @returns {Array.<module:heading/headingcommand~HeadingOption>}.
  107. */
  108. _getLocalizedOptions() {
  109. if ( this._cachedLocalizedOptions ) {
  110. return this._cachedLocalizedOptions;
  111. }
  112. const editor = this.editor;
  113. const t = editor.t;
  114. const localizedLabels = {
  115. Paragraph: t( 'Paragraph' ),
  116. 'Heading 1': t( 'Heading 1' ),
  117. 'Heading 2': t( 'Heading 2' ),
  118. 'Heading 3': t( 'Heading 3' )
  119. };
  120. /**
  121. * Cached localized version of `config.heading.options` generated by
  122. * {@link module:heading/headingengine~HeadingEngine#_localizedOptions}.
  123. *
  124. * @private
  125. * @readonly
  126. * @member {Array.<module:heading/headingcommand~HeadingOption>} #_cachedLocalizedOptions
  127. */
  128. this._cachedLocalizedOptions = editor.config.get( 'heading.options' )
  129. .map( option => {
  130. if ( localizedLabels[ option.label ] ) {
  131. // Clone the option to avoid altering the original `config.heading.options`.
  132. option = Object.assign( {}, option, {
  133. label: localizedLabels[ option.label ]
  134. } );
  135. }
  136. return option;
  137. } );
  138. return this._cachedLocalizedOptions;
  139. }
  140. }