paragraph.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. /**
  2. * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module paragraph/paragraph
  7. */
  8. import ParagraphCommand from './paragraphcommand';
  9. import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
  10. /**
  11. * The paragraph feature for the editor.
  12. *
  13. * It introduces the `<paragraph>` element in the model which renders as a `<p>` element in the DOM and data.
  14. *
  15. * @extends module:core/plugin~Plugin
  16. */
  17. export default class Paragraph extends Plugin {
  18. /**
  19. * @inheritDoc
  20. */
  21. static get pluginName() {
  22. return 'Paragraph';
  23. }
  24. /**
  25. * @inheritDoc
  26. */
  27. init() {
  28. const editor = this.editor;
  29. const model = editor.model;
  30. const data = editor.data;
  31. editor.commands.add( 'paragraph', new ParagraphCommand( editor ) );
  32. // Schema.
  33. model.schema.register( 'paragraph', { inheritAllFrom: '$block' } );
  34. editor.conversion.elementToElement( { model: 'paragraph', view: 'p' } );
  35. // Content autoparagraphing. --------------------------------------------------
  36. // Handles elements not converted by plugins and checks if would be converted if
  37. // we wraps them by a paragraph or changes them to a paragraph.
  38. data.upcastDispatcher.on( 'element', ( evt, data, conversionApi ) => {
  39. const writer = conversionApi.writer;
  40. // When element is already consumed by higher priority converters then do nothing.
  41. if ( !conversionApi.consumable.test( data.viewItem, { name: data.viewItem.name } ) ) {
  42. return;
  43. }
  44. // When element is paragraph-like lets try to change it into a paragraph.
  45. if ( Paragraph.paragraphLikeElements.has( data.viewItem.name ) ) {
  46. if ( data.viewItem.isEmpty ) {
  47. return;
  48. }
  49. const paragraph = writer.createElement( 'paragraph' );
  50. // Find allowed parent for paragraph that we are going to insert.
  51. // If current parent does not allow to insert paragraph but one of the ancestors does
  52. // then split nodes to allowed parent.
  53. const splitResult = conversionApi.splitToAllowedParent( paragraph, data.modelCursor );
  54. // When there is no split result it means that we can't insert paragraph in this position.
  55. if ( !splitResult ) {
  56. return;
  57. }
  58. // Insert paragraph in allowed position.
  59. writer.insert( paragraph, splitResult.position );
  60. // Convert children to paragraph.
  61. const { modelRange } = conversionApi.convertChildren( data.viewItem, writer.createPositionAt( paragraph, 0 ) );
  62. // Output range starts before paragraph but ends inside it after last child.
  63. // This is because we want to keep siblings inside the same paragraph as long as it is possible.
  64. // When next node won't be allowed in a paragraph it will split this paragraph anyway.
  65. data.modelRange = writer.createRange( writer.createPositionBefore( paragraph ), modelRange.end );
  66. data.modelCursor = data.modelRange.end;
  67. // When element is not paragraph-like lets try to wrap it by a paragraph.
  68. } else if ( isParagraphable( data.viewItem, data.modelCursor, conversionApi.schema ) ) {
  69. data = Object.assign( data, wrapInParagraph( data.viewItem, data.modelCursor, conversionApi ) );
  70. }
  71. }, { priority: 'low' } );
  72. // Handles not converted text nodes and checks if would be converted if we wraps then by a paragraph.
  73. data.upcastDispatcher.on( 'text', ( evt, data, conversionApi ) => {
  74. // When node is already converted then do nothing.
  75. if ( data.modelRange ) {
  76. return;
  77. }
  78. if ( isParagraphable( data.viewItem, data.modelCursor, conversionApi.schema ) ) {
  79. data = Object.assign( data, wrapInParagraph( data.viewItem, data.modelCursor, conversionApi ) );
  80. }
  81. }, { priority: 'lowest' } );
  82. // Empty roots autoparagraphing. -----------------------------------------------
  83. // Post-fixer which takes care of adding empty paragraph elements to empty roots.
  84. // Besides fixing content on #changesDone we also need to handle editor.data#ready event because
  85. // if initial data is empty or setData() wasn't even called there will be no #change fired.
  86. model.document.registerPostFixer( writer => this._autoparagraphEmptyRoots( writer ) );
  87. editor.data.on( 'ready', () => {
  88. model.enqueueChange( 'transparent', writer => this._autoparagraphEmptyRoots( writer ) );
  89. }, { priority: 'lowest' } );
  90. }
  91. /**
  92. * Fixes all empty roots.
  93. *
  94. * @private
  95. * @returns {Boolean} `true` if any change has been applied, `false` otherwise.
  96. */
  97. _autoparagraphEmptyRoots( writer ) {
  98. const model = this.editor.model;
  99. for ( const rootName of model.document.getRootNames() ) {
  100. const root = model.document.getRoot( rootName );
  101. if ( root.isEmpty && root.rootName != '$graveyard' ) {
  102. // If paragraph element is allowed in the root, create paragraph element.
  103. if ( model.schema.checkChild( root, 'paragraph' ) ) {
  104. writer.insertElement( 'paragraph', root );
  105. return true;
  106. }
  107. }
  108. }
  109. }
  110. }
  111. /**
  112. * A list of element names which should be treated by the autoparagraphing algorithms as
  113. * paragraph-like. This means that e.g. the following content:
  114. *
  115. * <h1>Foo</h1>
  116. * <table>
  117. * <tr>
  118. * <td>X</td>
  119. * <td>
  120. * <ul>
  121. * <li>Y</li>
  122. * <li>Z</li>
  123. * </ul>
  124. * </td>
  125. * </tr>
  126. * </table>
  127. *
  128. * contains five paragraph-like elements: `<h1>`, two `<td>`s and two `<li>`s.
  129. * Hence, if none of the features is going to convert those elements the above content will be automatically handled
  130. * by the paragraph feature and converted to:
  131. *
  132. * <p>Foo</p>
  133. * <p>X</p>
  134. * <p>Y</p>
  135. * <p>Z</p>
  136. *
  137. * Note: The `<td>` containing two `<li>` elements was ignored as the innermost paragraph-like elements
  138. * have a priority upon conversion.
  139. *
  140. * @member {Set.<String>} module:paragraph/paragraph~Paragraph.paragraphLikeElements
  141. */
  142. Paragraph.paragraphLikeElements = new Set( [
  143. 'blockquote',
  144. 'dd',
  145. 'div',
  146. 'dt',
  147. 'h1',
  148. 'h2',
  149. 'h3',
  150. 'h4',
  151. 'h5',
  152. 'h6',
  153. 'li',
  154. 'p',
  155. 'td'
  156. ] );
  157. function wrapInParagraph( input, position, conversionApi ) {
  158. const paragraph = conversionApi.writer.createElement( 'paragraph' );
  159. conversionApi.writer.insert( paragraph, position );
  160. return conversionApi.convertItem( input, conversionApi.writer.createPositionAt( paragraph, 0 ) );
  161. }
  162. function isParagraphable( node, position, schema ) {
  163. const context = schema.createContext( position );
  164. // When paragraph is allowed in this context...
  165. if ( !schema.checkChild( context, 'paragraph' ) ) {
  166. return false;
  167. }
  168. // And a node would be allowed in this paragraph...
  169. if ( !schema.checkChild( context.push( 'paragraph' ), node ) ) {
  170. return false;
  171. }
  172. return true;
  173. }