paragraph.js 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. /**
  2. * @license Copyright (c) 2003-2018, 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. import buildModelConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildmodelconverter';
  11. import buildViewConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildviewconverter';
  12. /**
  13. * The paragraph feature for the editor.
  14. * It introduces the `<paragraph>` element in the model which renders as a `<p>` element in the DOM and data.
  15. *
  16. * @extends module:core/plugin~Plugin
  17. */
  18. export default class Paragraph extends Plugin {
  19. /**
  20. * @inheritDoc
  21. */
  22. static get pluginName() {
  23. return 'Paragraph';
  24. }
  25. /**
  26. * @inheritDoc
  27. */
  28. init() {
  29. const editor = this.editor;
  30. const model = editor.model;
  31. const data = editor.data;
  32. const editing = editor.editing;
  33. /**
  34. * List of empty roots and batches that made them empty.
  35. *
  36. * @private
  37. * @type {Map<module:engine/model/rootelement~RootElement,module:engine/model/batch~Batch>}
  38. */
  39. this._rootsToFix = new Map();
  40. editor.commands.add( 'paragraph', new ParagraphCommand( editor ) );
  41. // Schema.
  42. model.schema.register( 'paragraph', { inheritAllFrom: '$block' } );
  43. // Build converter from model to view for data and editing pipelines.
  44. buildModelConverter().for( data.modelToView, editing.modelToView )
  45. .fromElement( 'paragraph' )
  46. .toElement( 'p' );
  47. // Build converter from view to model for data pipeline.
  48. buildViewConverter().for( data.viewToModel )
  49. .fromElement( 'p' )
  50. .toElement( 'paragraph' );
  51. // Content autoparagraphing. --------------------------------------------------
  52. // Step 1.
  53. // "Second chance" converters for elements and texts which were not allowed in their original locations.
  54. // They check if this element/text could be converted if it was in a paragraph.
  55. // Forcefully converted items will be temporarily in an invalid context. It's going to be fixed in step 2.
  56. // Executed after converter added by a feature, but before "default" to-model-fragment converter.
  57. data.viewToModel.on( 'element', convertAutoparagraphableItem, { priority: 'low' } );
  58. // Executed after default text converter.
  59. data.viewToModel.on( 'text', convertAutoparagraphableItem, { priority: 'lowest' } );
  60. // Step 2.
  61. // After an item is "forced" to be converted by `convertAutoparagraphableItem`, we need to actually take
  62. // care of adding the paragraph (assumed in `convertAutoparagraphableItem`) and wrap that item in it.
  63. // Executed after all converters (even default ones).
  64. data.viewToModel.on( 'element', autoparagraphItems, { priority: 'lowest' } );
  65. data.viewToModel.on( 'documentFragment', autoparagraphItems, { priority: 'lowest' } );
  66. // Empty roots autoparagraphing. -----------------------------------------------
  67. // Post-fixer which takes care of adding empty paragraph elements to empty roots.
  68. // Besides fixing content on #changesDone we also need to handle #dataReady because
  69. // if initial data is empty or setData() wasn't even called there will be no #change fired.
  70. model.document.registerPostFixer( writer => this._autoparagraphEmptyRoots( writer ) );
  71. editor.on( 'dataReady', () => {
  72. model.enqueueChange( 'transparent', writer => this._autoparagraphEmptyRoots( writer ) );
  73. }, { priority: 'lowest' } );
  74. }
  75. /**
  76. * Fixes all empty roots.
  77. *
  78. * @private
  79. * @returns {Boolean} `true` if any change has been applied, `false` otherwise.
  80. */
  81. _autoparagraphEmptyRoots( writer ) {
  82. const model = this.editor.model;
  83. for ( const rootName of model.document.getRootNames() ) {
  84. const root = model.document.getRoot( rootName );
  85. if ( root.isEmpty && root.rootName != '$graveyard' ) {
  86. // If paragraph element is allowed in the root, create paragraph element.
  87. if ( model.schema.checkChild( root, 'paragraph' ) ) {
  88. writer.insertElement( 'paragraph', root );
  89. return true;
  90. }
  91. }
  92. }
  93. }
  94. }
  95. /**
  96. * A list of element names which should be treated by the autoparagraphing algorithms as
  97. * paragraph-like. This means that e.g. the following content:
  98. *
  99. * <h1>Foo</h1>
  100. * <table>
  101. * <tr>
  102. * <td>X</td>
  103. * <td>
  104. * <ul>
  105. * <li>Y</li>
  106. * <li>Z</li>
  107. * </ul>
  108. * </td>
  109. * </tr>
  110. * </table>
  111. *
  112. * contains five paragraph-like elements: `<h1>`, two `<td>`s and two `<li>`s.
  113. * Hence, if none of the features is going to convert those elements the above content will be automatically handled
  114. * by the paragraph feature and converted to:
  115. *
  116. * <p>Foo</p>
  117. * <p>X</p>
  118. * <p>Y</p>
  119. * <p>Z</p>
  120. *
  121. * Note: The `<td>` containing two `<li>` elements was ignored as the innermost paragraph-like elements
  122. * have a priority upon conversion.
  123. *
  124. * @member {Set.<String>} module:paragraph/paragraph~Paragraph.paragraphLikeElements
  125. */
  126. Paragraph.paragraphLikeElements = new Set( [
  127. 'blockquote',
  128. 'dd',
  129. 'div',
  130. 'dt',
  131. 'h1',
  132. 'h2',
  133. 'h3',
  134. 'h4',
  135. 'h5',
  136. 'h6',
  137. 'li',
  138. 'p',
  139. 'td'
  140. ] );
  141. // This converter forces a conversion of a non-consumed view item, if that item would be allowed by schema and converted it if was
  142. // inside a paragraph element. The converter checks whether conversion would be possible if there was a paragraph element
  143. // between `data.input` item and its parent. If the conversion would be allowed, the converter adds `"paragraph"` to the
  144. // context and fires conversion for `data.input` again.
  145. function convertAutoparagraphableItem( evt, data, consumable, conversionApi ) {
  146. // If the item wasn't consumed by some of the dedicated converters...
  147. if ( !consumable.test( data.input, { name: data.input.name } ) ) {
  148. return;
  149. }
  150. // But would be allowed if it was in a paragraph...
  151. if ( !isParagraphable( data.input, data.context, conversionApi.schema, false ) ) {
  152. return;
  153. }
  154. // Convert that item in a paragraph context.
  155. data.context.push( 'paragraph' );
  156. const item = conversionApi.convertItem( data.input, consumable, data );
  157. data.context.pop();
  158. data.output = item;
  159. }
  160. // This converter checks all children of an element or document fragment that has been converted and wraps
  161. // children in a paragraph element if it is allowed by schema.
  162. //
  163. // Basically, after an item is "forced" to be converted by `convertAutoparagraphableItem`, we need to actually take
  164. // care of adding the paragraph (assumed in `convertAutoparagraphableItem`) and wrap that item in it.
  165. function autoparagraphItems( evt, data, consumable, conversionApi ) {
  166. // Autoparagraph only if the element has been converted.
  167. if ( !data.output ) {
  168. return;
  169. }
  170. const isParagraphLike = Paragraph.paragraphLikeElements.has( data.input.name ) && !data.output.is( 'element' );
  171. // Keep in mind that this converter is added to all elements and document fragments.
  172. // This means that we have to make a smart decision in which elements (at what level) auto-paragraph should be inserted.
  173. // There are three situations when it is correct to add paragraph:
  174. // - we are converting a view document fragment: this means that we are at the top level of conversion and we should
  175. // add paragraph elements for "bare" texts (unless converting in $clipboardHolder, but this is covered by schema),
  176. // - we are converting an element that was converted to model element: this means that it will be represented in model
  177. // and has added its context when converting children - we should add paragraph for those items that passed
  178. // in `convertAutoparagraphableItem`, because it is correct for them to be autoparagraphed,
  179. // - we are converting "paragraph-like" element, which children should always be autoparagraphed (if it is allowed by schema,
  180. // so we won't end up with, i.e., paragraph inside paragraph, if paragraph was in paragraph-like element).
  181. const shouldAutoparagraph =
  182. ( data.input.is( 'documentFragment' ) ) ||
  183. ( data.input.is( 'element' ) && data.output.is( 'element' ) ) ||
  184. isParagraphLike;
  185. if ( !shouldAutoparagraph ) {
  186. return;
  187. }
  188. // Take care of proper context. This is important for `isParagraphable` checks.
  189. const needsNewContext = data.output.is( 'element' );
  190. if ( needsNewContext ) {
  191. data.context.push( data.output );
  192. }
  193. // `paragraph` element that will wrap auto-paragraphable children.
  194. let autoParagraph = null;
  195. // Check children and wrap them in a `paragraph` element if they need to be wrapped.
  196. // Be smart when wrapping children and put all auto-paragraphable siblings in one `paragraph` parent:
  197. // foo<$text bold="true">bar</$text><paragraph>xxx</paragraph>baz --->
  198. // <paragraph>foo<$text bold="true">bar</$text></paragraph><paragraph>xxx</paragraph><paragraph>baz</paragraph>
  199. for ( let i = 0; i < data.output.childCount; i++ ) {
  200. const child = data.output.getChild( i );
  201. if ( isParagraphable( child, data.context, conversionApi.schema, isParagraphLike ) ) {
  202. // If there is no wrapping `paragraph` element, create it.
  203. if ( !autoParagraph ) {
  204. autoParagraph = conversionApi.writer.createElement( 'paragraph' );
  205. conversionApi.writer.insert( autoParagraph, data.output, child.index );
  206. }
  207. // Otherwise, use existing `paragraph` and just fix iterator.
  208. // Thanks to reusing `paragraph` element, multiple siblings ends up in same container.
  209. else {
  210. i--;
  211. }
  212. conversionApi.writer.append( child, autoParagraph );
  213. } else {
  214. // That was not a paragraphable children, reset `paragraph` wrapper - following auto-paragraphable children
  215. // need to be placed in a new `paragraph` element.
  216. autoParagraph = null;
  217. }
  218. }
  219. if ( needsNewContext ) {
  220. data.context.pop();
  221. }
  222. }
  223. function isParagraphable( node, context, schema, insideParagraphLikeElement ) {
  224. // Node is paragraphable if it is inside paragraph like element, or...
  225. // It is not allowed at this context...
  226. if ( !insideParagraphLikeElement && schema.checkChild( context, node ) ) {
  227. return false;
  228. }
  229. // And paragraph is allowed in this context...
  230. if ( !schema.checkChild( context, 'paragraph' ) ) {
  231. return false;
  232. }
  233. // And a node would be allowed in this paragraph...
  234. if ( !schema.checkChild( [ ...context, 'paragraph' ], node ) ) {
  235. return false;
  236. }
  237. return true;
  238. }