@ckeditor/ckeditor5-paragraph
Advanced tools
Comparing version 1.0.0-alpha.2 to 1.0.0-beta.1
Changelog | ||
========= | ||
## [1.0.0-beta.1](https://github.com/ckeditor/ckeditor5-paragraph/compare/v1.0.0-alpha.2...v1.0.0-beta.1) (2018-03-15) | ||
### Other changes | ||
* Aligned feature class naming to the new scheme. ([69e98d3](https://github.com/ckeditor/ckeditor5-paragraph/commit/69e98d3)) | ||
## [1.0.0-alpha.2](https://github.com/ckeditor/ckeditor5-paragraph/compare/v1.0.0-alpha.1...v1.0.0-alpha.2) (2017-11-14) | ||
@@ -5,0 +12,0 @@ |
@@ -5,3 +5,3 @@ Software License Agreement | ||
**CKEditor 5 Paragraph Feature** – https://github.com/ckeditor/ckeditor5-paragraph <br> | ||
Copyright (c) 2003-2017, [CKSource](http://cksource.com) Frederico Knabben. All rights reserved. | ||
Copyright (c) 2003-2018, [CKSource](http://cksource.com) Frederico Knabben. All rights reserved. | ||
@@ -8,0 +8,0 @@ Licensed under the terms of any of the following licenses at your choice: |
{ | ||
"name": "@ckeditor/ckeditor5-paragraph", | ||
"version": "1.0.0-alpha.2", | ||
"version": "1.0.0-beta.1", | ||
"description": "Paragraph feature for CKEditor 5.", | ||
@@ -10,19 +10,19 @@ "keywords": [ | ||
"dependencies": { | ||
"@ckeditor/ckeditor5-core": "^1.0.0-alpha.2", | ||
"@ckeditor/ckeditor5-engine": "^1.0.0-alpha.2", | ||
"@ckeditor/ckeditor5-utils": "^1.0.0-alpha.2" | ||
"@ckeditor/ckeditor5-core": "^1.0.0-beta.1", | ||
"@ckeditor/ckeditor5-engine": "^1.0.0-beta.1", | ||
"@ckeditor/ckeditor5-utils": "^1.0.0-beta.1" | ||
}, | ||
"devDependencies": { | ||
"@ckeditor/ckeditor5-basic-styles": "^1.0.0-alpha.2", | ||
"@ckeditor/ckeditor5-clipboard": "^1.0.0-alpha.2", | ||
"@ckeditor/ckeditor5-editor-classic": "^1.0.0-alpha.2", | ||
"@ckeditor/ckeditor5-enter": "^1.0.0-alpha.2", | ||
"@ckeditor/ckeditor5-heading": "^1.0.0-alpha.2", | ||
"@ckeditor/ckeditor5-link": "^1.0.0-alpha.2", | ||
"@ckeditor/ckeditor5-typing": "^1.0.0-alpha.2", | ||
"@ckeditor/ckeditor5-undo": "^1.0.0-alpha.2", | ||
"eslint": "^4.8.0", | ||
"eslint-config-ckeditor5": "^1.0.6", | ||
"@ckeditor/ckeditor5-basic-styles": "^1.0.0-beta.1", | ||
"@ckeditor/ckeditor5-clipboard": "^1.0.0-beta.1", | ||
"@ckeditor/ckeditor5-editor-classic": "^1.0.0-beta.1", | ||
"@ckeditor/ckeditor5-enter": "^1.0.0-beta.1", | ||
"@ckeditor/ckeditor5-heading": "^1.0.0-beta.1", | ||
"@ckeditor/ckeditor5-link": "^1.0.0-beta.1", | ||
"@ckeditor/ckeditor5-typing": "^1.0.0-beta.1", | ||
"@ckeditor/ckeditor5-undo": "^1.0.0-beta.1", | ||
"eslint": "^4.15.0", | ||
"eslint-config-ckeditor5": "^1.0.7", | ||
"husky": "^0.14.3", | ||
"lint-staged": "^4.2.3" | ||
"lint-staged": "^6.0.0" | ||
}, | ||
@@ -29,0 +29,0 @@ "engines": { |
@@ -7,3 +7,5 @@ CKEditor 5 paragraph feature | ||
[![Build Status](https://travis-ci.org/ckeditor/ckeditor5-paragraph.svg?branch=master)](https://travis-ci.org/ckeditor/ckeditor5-paragraph) | ||
[![Test Coverage](https://codeclimate.com/github/ckeditor/ckeditor5-paragraph/badges/coverage.svg)](https://codeclimate.com/github/ckeditor/ckeditor5-paragraph/coverage) | ||
[![BrowserStack Status](https://www.browserstack.com/automate/badge.svg?badge_key=d3hvenZqQVZERFQ5d09FWXdyT0ozVXhLaVltRFRjTTUyZGpvQWNmWVhUUT0tLUZqNlJ1YWRUd0RvdEVOaEptM1B2Q0E9PQ==--c9d3dee40b9b4471ff3fb516d9ecf8d09292c7e0)](https://www.browserstack.com/automate/public-build/d3hvenZqQVZERFQ5d09FWXdyT0ozVXhLaVltRFRjTTUyZGpvQWNmWVhUUT0tLUZqNlJ1YWRUd0RvdEVOaEptM1B2Q0E9PQ==--c9d3dee40b9b4471ff3fb516d9ecf8d09292c7e0) | ||
[![Coverage Status](https://coveralls.io/repos/github/ckeditor/ckeditor5-paragraph/badge.svg?branch=master)](https://coveralls.io/github/ckeditor/ckeditor5-paragraph?branch=master) | ||
<br> | ||
[![Dependency Status](https://david-dm.org/ckeditor/ckeditor5-paragraph/status.svg)](https://david-dm.org/ckeditor/ckeditor5-paragraph) | ||
@@ -10,0 +12,0 @@ [![devDependency Status](https://david-dm.org/ckeditor/ckeditor5-paragraph/dev-status.svg)](https://david-dm.org/ckeditor/ckeditor5-paragraph?type=dev) |
/** | ||
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | ||
* @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. | ||
* For licensing, see LICENSE.md. | ||
@@ -11,10 +11,8 @@ */ | ||
import ParagraphCommand from './paragraphcommand'; | ||
import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; | ||
import { SchemaContext } from '@ckeditor/ckeditor5-engine/src/model/schema'; | ||
import Position from '@ckeditor/ckeditor5-engine/src/model/position'; | ||
import Range from '@ckeditor/ckeditor5-engine/src/model/range'; | ||
import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element'; | ||
import ModelPosition from '@ckeditor/ckeditor5-engine/src/model/position'; | ||
import buildModelConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildmodelconverter'; | ||
import buildViewConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildviewconverter'; | ||
/** | ||
@@ -39,5 +37,4 @@ * The paragraph feature for the editor. | ||
const editor = this.editor; | ||
const doc = editor.document; | ||
const model = editor.model; | ||
const data = editor.data; | ||
const editing = editor.editing; | ||
@@ -47,34 +44,64 @@ editor.commands.add( 'paragraph', new ParagraphCommand( editor ) ); | ||
// Schema. | ||
doc.schema.registerItem( 'paragraph', '$block' ); | ||
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' ); | ||
editor.conversion.elementToElement( { model: 'paragraph', view: '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. | ||
// Handles elements not converted by plugins and checks if would be converted if | ||
// we wraps them by a paragraph or changes them to a paragraph. | ||
data.upcastDispatcher.on( 'element', ( evt, data, conversionApi ) => { | ||
// When element is already consumed by higher priority converters then do nothing. | ||
if ( !conversionApi.consumable.test( data.viewItem, { name: data.viewItem.name } ) ) { | ||
return; | ||
} | ||
// 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' } ); | ||
// When element is paragraph-like lets try to change it into a paragraph. | ||
if ( Paragraph.paragraphLikeElements.has( data.viewItem.name ) ) { | ||
if ( data.viewItem.isEmpty ) { | ||
return; | ||
} | ||
// 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. | ||
const paragraph = conversionApi.writer.createElement( 'paragraph' ); | ||
// Executed after all converters (even default ones). | ||
data.viewToModel.on( 'element', autoparagraphItems, { priority: 'lowest' } ); | ||
data.viewToModel.on( 'documentFragment', autoparagraphItems, { priority: 'lowest' } ); | ||
// Find allowed parent for paragraph that we are going to insert. | ||
// If current parent does not allow to insert paragraph but one of the ancestors does | ||
// then split nodes to allowed parent. | ||
const splitResult = conversionApi.splitToAllowedParent( paragraph, data.modelCursor ); | ||
// When there is no split result it means that we can't insert paragraph in this position. | ||
if ( !splitResult ) { | ||
return; | ||
} | ||
// Insert paragraph in allowed position. | ||
conversionApi.writer.insert( paragraph, splitResult.position ); | ||
// Convert children to paragraph. | ||
const { modelRange } = conversionApi.convertChildren( data.viewItem, Position.createAt( paragraph ) ); | ||
// Output range starts before paragraph but ends inside it after last child. | ||
// This is because we want to keep siblings inside the same paragraph as long as it is possible. | ||
// When next node won't be allowed in a paragraph it will split this paragraph anyway. | ||
data.modelRange = new Range( Position.createBefore( paragraph ), modelRange.end ); | ||
data.modelCursor = data.modelRange.end; | ||
// When element is not paragraph-like lets try to wrap it by a paragraph. | ||
} else if ( isParagraphable( data.viewItem, data.modelCursor, conversionApi.schema ) ) { | ||
data = Object.assign( data, wrapInParagraph( data.viewItem, data.modelCursor, conversionApi ) ); | ||
} | ||
}, { priority: 'low' } ); | ||
// Handles not converted text nodes and checks if would be converted if we wraps then by a paragraph. | ||
data.upcastDispatcher.on( 'text', ( evt, data, conversionApi ) => { | ||
// When node is already converted then do nothing. | ||
if ( data.modelRange ) { | ||
return; | ||
} | ||
if ( isParagraphable( data.viewItem, data.modelCursor, conversionApi.schema ) ) { | ||
data = Object.assign( data, wrapInParagraph( data.viewItem, data.modelCursor, conversionApi ) ); | ||
} | ||
}, { priority: 'lowest' } ); | ||
// Empty roots autoparagraphing. ----------------------------------------------- | ||
@@ -85,15 +112,31 @@ | ||
// if initial data is empty or setData() wasn't even called there will be no #change fired. | ||
doc.on( 'change', ( evt, type, changes, batch ) => { | ||
if ( batch.type == 'transparent' ) { | ||
return; | ||
} | ||
model.document.registerPostFixer( writer => this._autoparagraphEmptyRoots( writer ) ); | ||
findEmptyRoots( doc, batch ); | ||
} ); | ||
doc.on( 'changesDone', autoparagraphEmptyRoots, { priority: 'lowest' } ); | ||
editor.on( 'dataReady', () => { | ||
findEmptyRoots( doc, doc.batch( 'transparent' ) ); | ||
autoparagraphEmptyRoots(); | ||
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; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
@@ -148,116 +191,19 @@ | ||
// 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; | ||
} | ||
function wrapInParagraph( input, position, conversionApi ) { | ||
const paragraph = conversionApi.writer.createElement( 'paragraph' ); | ||
// 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; | ||
conversionApi.writer.insert( paragraph, position ); | ||
return conversionApi.convertItem( input, Position.createAt( paragraph ) ); | ||
} | ||
// 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; | ||
} | ||
function isParagraphable( node, position, schema ) { | ||
const context = new SchemaContext( position ); | ||
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 = new ModelElement( 'paragraph' ); | ||
data.output.insertChildren( child.index, autoParagraph ); | ||
} | ||
// Otherwise, use existing `paragraph` and just fix iterator. | ||
// Thanks to reusing `paragraph` element, multiple siblings ends up in same container. | ||
else { | ||
i--; | ||
} | ||
child.remove(); | ||
autoParagraph.appendChildren( child ); | ||
} 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 ) { | ||
const name = node.name || '$text'; | ||
// Node is paragraphable if it is inside paragraph like element, or... | ||
// It is not allowed at this context... | ||
if ( !insideParagraphLikeElement && schema.check( { name, inside: context } ) ) { | ||
// When paragraph is allowed in this context... | ||
if ( !schema.checkChild( context, 'paragraph' ) ) { | ||
return false; | ||
} | ||
// And paragraph is allowed in this context... | ||
if ( !schema.check( { name: 'paragraph', inside: context } ) ) { | ||
return false; | ||
} | ||
// And a node would be allowed in this paragraph... | ||
if ( !schema.check( { name, inside: context.concat( 'paragraph' ) } ) ) { | ||
if ( !schema.checkChild( context.push( 'paragraph' ), node ) ) { | ||
return false; | ||
@@ -268,41 +214,1 @@ } | ||
} | ||
// Looks through all roots created in document and marks every empty root, saving which batch made it empty. | ||
const rootsToFix = new Map(); | ||
function findEmptyRoots( doc, batch ) { | ||
for ( const rootName of doc.getRootNames() ) { | ||
const root = doc.getRoot( rootName ); | ||
if ( root.isEmpty ) { | ||
if ( !rootsToFix.has( root ) ) { | ||
rootsToFix.set( root, batch ); | ||
} | ||
} else { | ||
rootsToFix.delete( root ); | ||
} | ||
} | ||
} | ||
// Fixes all empty roots. | ||
function autoparagraphEmptyRoots() { | ||
for ( const [ root, batch ] of rootsToFix ) { | ||
// Only empty roots are in `rootsToFix`. Even if root got content during `changesDone` event (because of, for example | ||
// other feature), this will fire `findEmptyRoots` and remove that root from `rootsToFix`. So we are guaranteed | ||
// to have only empty roots here. | ||
const query = { name: 'paragraph', inside: [ root ] }; | ||
const doc = batch.document; | ||
const schema = doc.schema; | ||
// If paragraph element is allowed in the root, create paragraph element. | ||
if ( schema.check( query ) ) { | ||
doc.enqueueChanges( () => { | ||
// Remove root from `rootsToFix` here, before executing batch, to prevent infinite loops. | ||
rootsToFix.delete( root ); | ||
// Fix empty root. | ||
batch.insert( ModelPosition.createAt( root ), new ModelElement( 'paragraph' ) ); | ||
} ); | ||
} | ||
} | ||
} |
/** | ||
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | ||
* @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. | ||
* For licensing, see LICENSE.md. | ||
@@ -11,3 +11,2 @@ */ | ||
import Command from '@ckeditor/ckeditor5-core/src/command'; | ||
import Position from '@ckeditor/ckeditor5-engine/src/model/position'; | ||
import first from '@ckeditor/ckeditor5-utils/src/first'; | ||
@@ -33,7 +32,8 @@ | ||
refresh() { | ||
const document = this.editor.document; | ||
const model = this.editor.model; | ||
const document = model.document; | ||
const block = first( document.selection.getSelectedBlocks() ); | ||
this.value = !!block && block.is( 'paragraph' ); | ||
this.isEnabled = !!block && checkCanBecomeParagraph( block, document.schema ); | ||
this.isEnabled = !!block && checkCanBecomeParagraph( block, model.schema ); | ||
} | ||
@@ -47,4 +47,2 @@ | ||
* @param {Object} [options] Options for the executed command. | ||
* @param {module:engine/model/batch~Batch} [options.batch] A batch to collect all the change steps. | ||
* A new batch will be created if this option is not set. | ||
* @param {module:engine/model/selection~Selection} [options.selection] The selection that the command should be applied to. | ||
@@ -54,11 +52,11 @@ * By default, if not provided, the command is applied to the {@link module:engine/model/document~Document#selection}. | ||
execute( options = {} ) { | ||
const document = this.editor.document; | ||
const model = this.editor.model; | ||
const document = model.document; | ||
document.enqueueChanges( () => { | ||
const batch = options.batch || document.batch(); | ||
model.change( writer => { | ||
const blocks = ( options.selection || document.selection ).getSelectedBlocks(); | ||
for ( const block of blocks ) { | ||
if ( !block.is( 'paragraph' ) && checkCanBecomeParagraph( block, document.schema ) ) { | ||
batch.rename( block, 'paragraph' ); | ||
if ( !block.is( 'paragraph' ) && checkCanBecomeParagraph( block, model.schema ) ) { | ||
writer.rename( block, 'paragraph' ); | ||
} | ||
@@ -77,6 +75,3 @@ } | ||
function checkCanBecomeParagraph( block, schema ) { | ||
return schema.check( { | ||
name: 'paragraph', | ||
inside: Position.createBefore( block ) | ||
} ); | ||
return schema.checkChild( block.parent, 'paragraph' ) && !schema.isObject( block ); | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
22
17239
237
1