8
0

paragraph.js 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. /**
  2. * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  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 element which has not been converted by any plugin and checks if it would be converted if
  37. // we wrap it in a paragraph or change it to a paragraph.
  38. editor.conversion.for( 'upcast' ).elementToElement( {
  39. model: ( viewElement, modelWriter ) => {
  40. if ( !Paragraph.paragraphLikeElements.has( viewElement.name ) ) {
  41. return null;
  42. }
  43. // Do not auto-paragraph empty elements.
  44. if ( viewElement.isEmpty ) {
  45. return null;
  46. }
  47. return modelWriter.createElement( 'paragraph' );
  48. },
  49. converterPriority: 'low'
  50. } );
  51. data.upcastDispatcher.on( 'element', ( evt, data, conversionApi ) => {
  52. // Do not try auto-paragraphing if the element was already converted.
  53. if ( !conversionApi.consumable.test( data.viewItem, { name: data.viewItem.name } ) ) {
  54. return;
  55. }
  56. // If the element is not paragraph-like try wrapping it in a paragraph.
  57. if ( isParagraphable( data.viewItem, data.modelCursor, conversionApi.schema ) ) {
  58. Object.assign( data, wrapInParagraph( data.viewItem, data.modelCursor, conversionApi ) );
  59. }
  60. }, { priority: 'low' } );
  61. // Handles not converted text nodes and checks if would be converted if we wraps then by a paragraph.
  62. data.upcastDispatcher.on( 'text', ( evt, data, conversionApi ) => {
  63. // When node is already converted then do nothing.
  64. if ( data.modelRange ) {
  65. return;
  66. }
  67. if ( isParagraphable( data.viewItem, data.modelCursor, conversionApi.schema ) ) {
  68. Object.assign( data, wrapInParagraph( data.viewItem, data.modelCursor, conversionApi ) );
  69. }
  70. }, { priority: 'lowest' } );
  71. // Empty roots autoparagraphing. -----------------------------------------------
  72. // Post-fixer which takes care of adding empty paragraph elements to empty roots.
  73. // Besides fixing content on #changesDone we also need to handle editor.data#ready event because
  74. // if initial data is empty or setData() wasn't even called there will be no #change fired.
  75. model.document.registerPostFixer( writer => this._autoparagraphEmptyRoots( writer ) );
  76. editor.data.on( 'ready', () => {
  77. model.enqueueChange( 'transparent', writer => this._autoparagraphEmptyRoots( writer ) );
  78. }, { priority: 'lowest' } );
  79. }
  80. /**
  81. * Fixes all empty roots.
  82. *
  83. * @private
  84. * @returns {Boolean} `true` if any change has been applied, `false` otherwise.
  85. */
  86. _autoparagraphEmptyRoots( writer ) {
  87. const model = this.editor.model;
  88. for ( const rootName of model.document.getRootNames() ) {
  89. const root = model.document.getRoot( rootName );
  90. if ( root.isEmpty && root.rootName != '$graveyard' ) {
  91. // If paragraph element is allowed in the root, create paragraph element.
  92. if ( model.schema.checkChild( root, 'paragraph' ) ) {
  93. writer.insertElement( 'paragraph', root );
  94. return true;
  95. }
  96. }
  97. }
  98. }
  99. }
  100. /**
  101. * A list of element names which should be treated by the autoparagraphing algorithms as
  102. * paragraph-like. This means that e.g. the following content:
  103. *
  104. * <h1>Foo</h1>
  105. * <table>
  106. * <tr>
  107. * <td>X</td>
  108. * <td>
  109. * <ul>
  110. * <li>Y</li>
  111. * <li>Z</li>
  112. * </ul>
  113. * </td>
  114. * </tr>
  115. * </table>
  116. *
  117. * contains five paragraph-like elements: `<h1>`, two `<td>`s and two `<li>`s.
  118. * Hence, if none of the features is going to convert those elements the above content will be automatically handled
  119. * by the paragraph feature and converted to:
  120. *
  121. * <p>Foo</p>
  122. * <p>X</p>
  123. * <p>Y</p>
  124. * <p>Z</p>
  125. *
  126. * Note: The `<td>` containing two `<li>` elements was ignored as the innermost paragraph-like elements
  127. * have a priority upon conversion.
  128. *
  129. * @member {Set.<String>} module:paragraph/paragraph~Paragraph.paragraphLikeElements
  130. */
  131. Paragraph.paragraphLikeElements = new Set( [
  132. 'blockquote',
  133. 'dd',
  134. 'div',
  135. 'dt',
  136. 'h1',
  137. 'h2',
  138. 'h3',
  139. 'h4',
  140. 'h5',
  141. 'h6',
  142. 'li',
  143. 'p',
  144. 'td'
  145. ] );
  146. function wrapInParagraph( input, position, conversionApi ) {
  147. const paragraph = conversionApi.writer.createElement( 'paragraph' );
  148. conversionApi.writer.insert( paragraph, position );
  149. return conversionApi.convertItem( input, conversionApi.writer.createPositionAt( paragraph, 0 ) );
  150. }
  151. function isParagraphable( node, position, schema ) {
  152. const context = schema.createContext( position );
  153. // When paragraph is allowed in this context...
  154. if ( !schema.checkChild( context, 'paragraph' ) ) {
  155. return false;
  156. }
  157. // And a node would be allowed in this paragraph...
  158. if ( !schema.checkChild( context.push( 'paragraph' ), node ) ) {
  159. return false;
  160. }
  161. return true;
  162. }