| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280 |
- /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
- /**
- * @module paragraph/paragraph
- */
- import ParagraphCommand from './paragraphcommand';
- import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
- import buildModelConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildmodelconverter';
- import buildViewConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildviewconverter';
- /**
- * The paragraph feature for the editor.
- * It introduces the `<paragraph>` element in the model which renders as a `<p>` element in the DOM and data.
- *
- * @extends module:core/plugin~Plugin
- */
- export default class Paragraph extends Plugin {
- /**
- * @inheritDoc
- */
- static get pluginName() {
- return 'Paragraph';
- }
- /**
- * @inheritDoc
- */
- init() {
- const editor = this.editor;
- const model = editor.model;
- const data = editor.data;
- const editing = editor.editing;
- /**
- * List of empty roots and batches that made them empty.
- *
- * @private
- * @type {Map<module:engine/model/rootelement~RootElement,module:engine/model/batch~Batch>}
- */
- this._rootsToFix = new Map();
- editor.commands.add( 'paragraph', new ParagraphCommand( editor ) );
- // Schema.
- model.schema.register( 'paragraph', { inheritAllFrom: '$block' } );
- // Build converter from model to view for data and editing pipelines.
- buildModelConverter().for( data.modelToView, editing.modelToView )
- .fromElement( 'paragraph' )
- .toElement( 'p' );
- // Build converter from view to model for data pipeline.
- buildViewConverter().for( data.viewToModel )
- .fromElement( 'p' )
- .toElement( 'paragraph' );
- // Content autoparagraphing. --------------------------------------------------
- // Step 1.
- // "Second chance" converters for elements and texts which were not allowed in their original locations.
- // They check if this element/text could be converted if it was in a paragraph.
- // Forcefully converted items will be temporarily in an invalid context. It's going to be fixed in step 2.
- // Executed after converter added by a feature, but before "default" to-model-fragment converter.
- data.viewToModel.on( 'element', convertAutoparagraphableItem, { priority: 'low' } );
- // Executed after default text converter.
- data.viewToModel.on( 'text', convertAutoparagraphableItem, { priority: 'lowest' } );
- // Step 2.
- // After an item is "forced" to be converted by `convertAutoparagraphableItem`, we need to actually take
- // care of adding the paragraph (assumed in `convertAutoparagraphableItem`) and wrap that item in it.
- // Executed after all converters (even default ones).
- data.viewToModel.on( 'element', autoparagraphItems, { priority: 'lowest' } );
- data.viewToModel.on( 'documentFragment', autoparagraphItems, { priority: 'lowest' } );
- // Empty roots autoparagraphing. -----------------------------------------------
- // Post-fixer which takes care of adding empty paragraph elements to empty roots.
- // Besides fixing content on #changesDone we also need to handle #dataReady because
- // if initial data is empty or setData() wasn't even called there will be no #change fired.
- model.document.registerPostFixer( writer => this._autoparagraphEmptyRoots( writer ) );
- editor.on( 'dataReady', () => {
- model.enqueueChange( 'transparent', writer => this._autoparagraphEmptyRoots( writer ) );
- }, { priority: 'lowest' } );
- }
- /**
- * Fixes all empty roots.
- *
- * @private
- * @returns {Boolean} `true` if any change has been applied, `false` otherwise.
- */
- _autoparagraphEmptyRoots( writer ) {
- const model = this.editor.model;
- for ( const rootName of model.document.getRootNames() ) {
- const root = model.document.getRoot( rootName );
- if ( root.isEmpty && root.rootName != '$graveyard' ) {
- // If paragraph element is allowed in the root, create paragraph element.
- if ( model.schema.checkChild( root, 'paragraph' ) ) {
- writer.insertElement( 'paragraph', root );
- return true;
- }
- }
- }
- }
- }
- /**
- * A list of element names which should be treated by the autoparagraphing algorithms as
- * paragraph-like. This means that e.g. the following content:
- *
- * <h1>Foo</h1>
- * <table>
- * <tr>
- * <td>X</td>
- * <td>
- * <ul>
- * <li>Y</li>
- * <li>Z</li>
- * </ul>
- * </td>
- * </tr>
- * </table>
- *
- * contains five paragraph-like elements: `<h1>`, two `<td>`s and two `<li>`s.
- * Hence, if none of the features is going to convert those elements the above content will be automatically handled
- * by the paragraph feature and converted to:
- *
- * <p>Foo</p>
- * <p>X</p>
- * <p>Y</p>
- * <p>Z</p>
- *
- * Note: The `<td>` containing two `<li>` elements was ignored as the innermost paragraph-like elements
- * have a priority upon conversion.
- *
- * @member {Set.<String>} module:paragraph/paragraph~Paragraph.paragraphLikeElements
- */
- Paragraph.paragraphLikeElements = new Set( [
- 'blockquote',
- 'dd',
- 'div',
- 'dt',
- 'h1',
- 'h2',
- 'h3',
- 'h4',
- 'h5',
- 'h6',
- 'li',
- 'p',
- 'td'
- ] );
- // This converter forces a conversion of a non-consumed view item, if that item would be allowed by schema and converted it if was
- // inside a paragraph element. The converter checks whether conversion would be possible if there was a paragraph element
- // between `data.input` item and its parent. If the conversion would be allowed, the converter adds `"paragraph"` to the
- // context and fires conversion for `data.input` again.
- function convertAutoparagraphableItem( evt, data, consumable, conversionApi ) {
- // If the item wasn't consumed by some of the dedicated converters...
- if ( !consumable.test( data.input, { name: data.input.name } ) ) {
- return;
- }
- // But would be allowed if it was in a paragraph...
- if ( !isParagraphable( data.input, data.context, conversionApi.schema, false ) ) {
- return;
- }
- // Convert that item in a paragraph context.
- data.context.push( 'paragraph' );
- const item = conversionApi.convertItem( data.input, consumable, data );
- data.context.pop();
- data.output = item;
- }
- // This converter checks all children of an element or document fragment that has been converted and wraps
- // children in a paragraph element if it is allowed by schema.
- //
- // Basically, after an item is "forced" to be converted by `convertAutoparagraphableItem`, we need to actually take
- // care of adding the paragraph (assumed in `convertAutoparagraphableItem`) and wrap that item in it.
- function autoparagraphItems( evt, data, consumable, conversionApi ) {
- // Autoparagraph only if the element has been converted.
- if ( !data.output ) {
- return;
- }
- const isParagraphLike = Paragraph.paragraphLikeElements.has( data.input.name ) && !data.output.is( 'element' );
- // Keep in mind that this converter is added to all elements and document fragments.
- // This means that we have to make a smart decision in which elements (at what level) auto-paragraph should be inserted.
- // There are three situations when it is correct to add paragraph:
- // - we are converting a view document fragment: this means that we are at the top level of conversion and we should
- // add paragraph elements for "bare" texts (unless converting in $clipboardHolder, but this is covered by schema),
- // - we are converting an element that was converted to model element: this means that it will be represented in model
- // and has added its context when converting children - we should add paragraph for those items that passed
- // in `convertAutoparagraphableItem`, because it is correct for them to be autoparagraphed,
- // - we are converting "paragraph-like" element, which children should always be autoparagraphed (if it is allowed by schema,
- // so we won't end up with, i.e., paragraph inside paragraph, if paragraph was in paragraph-like element).
- const shouldAutoparagraph =
- ( data.input.is( 'documentFragment' ) ) ||
- ( data.input.is( 'element' ) && data.output.is( 'element' ) ) ||
- isParagraphLike;
- if ( !shouldAutoparagraph ) {
- return;
- }
- // Take care of proper context. This is important for `isParagraphable` checks.
- const needsNewContext = data.output.is( 'element' );
- if ( needsNewContext ) {
- data.context.push( data.output );
- }
- // `paragraph` element that will wrap auto-paragraphable children.
- let autoParagraph = null;
- // Check children and wrap them in a `paragraph` element if they need to be wrapped.
- // Be smart when wrapping children and put all auto-paragraphable siblings in one `paragraph` parent:
- // foo<$text bold="true">bar</$text><paragraph>xxx</paragraph>baz --->
- // <paragraph>foo<$text bold="true">bar</$text></paragraph><paragraph>xxx</paragraph><paragraph>baz</paragraph>
- for ( let i = 0; i < data.output.childCount; i++ ) {
- const child = data.output.getChild( i );
- if ( isParagraphable( child, data.context, conversionApi.schema, isParagraphLike ) ) {
- // If there is no wrapping `paragraph` element, create it.
- if ( !autoParagraph ) {
- autoParagraph = conversionApi.writer.createElement( 'paragraph' );
- conversionApi.writer.insert( autoParagraph, data.output, child.index );
- }
- // Otherwise, use existing `paragraph` and just fix iterator.
- // Thanks to reusing `paragraph` element, multiple siblings ends up in same container.
- else {
- i--;
- }
- conversionApi.writer.append( child, autoParagraph );
- } else {
- // That was not a paragraphable children, reset `paragraph` wrapper - following auto-paragraphable children
- // need to be placed in a new `paragraph` element.
- autoParagraph = null;
- }
- }
- if ( needsNewContext ) {
- data.context.pop();
- }
- }
- function isParagraphable( node, context, schema, insideParagraphLikeElement ) {
- // Node is paragraphable if it is inside paragraph like element, or...
- // It is not allowed at this context...
- if ( !insideParagraphLikeElement && schema.checkChild( context, node ) ) {
- return false;
- }
- // And paragraph is allowed in this context...
- if ( !schema.checkChild( context, 'paragraph' ) ) {
- return false;
- }
- // And a node would be allowed in this paragraph...
- if ( !schema.checkChild( [ ...context, 'paragraph' ], node ) ) {
- return false;
- }
- return true;
- }
|