Skip to content
This repository has been archived by the owner on Jun 26, 2020. It is now read-only.

Commit

Permalink
Merge pull request #1203 from ckeditor/t/1186
Browse files Browse the repository at this point in the history
Other: Refactor: engine/model reorganization, introducing new chnage and enqueueChange block, split batch/writer. Related: #1186.
  • Loading branch information
Piotr Jasiun authored Dec 14, 2017
2 parents aba8e68 + 9bc974a commit 5be1ad6
Show file tree
Hide file tree
Showing 113 changed files with 5,618 additions and 4,961 deletions.
51 changes: 21 additions & 30 deletions src/controller/datacontroller.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,15 @@ export default class DataController {
/**
* Creates data controller instance.
*
* @param {module:engine/model/document~Document} model Document model.
* @param {module:engine/model/model~Model} model Data model.
* @param {module:engine/dataprocessor/dataprocessor~DataProcessor} [dataProcessor] Data processor which should used by the controller.
*/
constructor( model, dataProcessor ) {
/**
* Document model.
* Data model.
*
* @readonly
* @member {module:engine/model/document~Document}
* @member {module:engine/model/model~Model}
*/
this.model = model;

Expand Down Expand Up @@ -104,7 +104,7 @@ export default class DataController {
* @readonly
* @member {module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher}
*/
this.viewToModel = new ViewConversionDispatcher( {
this.viewToModel = new ViewConversionDispatcher( this.model, {
schema: model.schema
} );

Expand All @@ -130,7 +130,7 @@ export default class DataController {
*/
get( rootName = 'main' ) {
// Get model range.
return this.stringify( this.model.getRoot( rootName ) );
return this.stringify( this.model.document.getRoot( rootName ) );
}

/**
Expand Down Expand Up @@ -186,19 +186,16 @@ export default class DataController {
*/
set( data, rootName = 'main' ) {
// Save to model.
const modelRoot = this.model.getRoot( rootName );
const modelRoot = this.model.document.getRoot( rootName );

this.model.enqueueChanges( () => {
this.model.enqueueChange( 'transparent', writer => {
// Clearing selection is a workaround for ticket #569 (LiveRange loses position after removing data from document).
// After fixing it this code should be removed.
this.model.selection.removeAllRanges();
this.model.selection.clearAttributes();
this.model.document.selection.removeAllRanges();
this.model.document.selection.clearAttributes();

// Initial batch should be ignored by features like undo, etc.
const batch = this.model.batch( 'transparent' );

batch.remove( ModelRange.createIn( modelRoot ) );
batch.insert( this.parse( data, batch ), modelRoot );
writer.remove( ModelRange.createIn( modelRoot ) );
writer.insert( this.parse( data ), modelRoot );
} );
}

Expand All @@ -208,17 +205,16 @@ export default class DataController {
*
* @see #set
* @param {String} data Data to parse.
* @param {module:engine/model/batch~Batch} batch Batch to which the deltas will be added.
* @param {String} [context='$root'] Base context in which the view will be converted to the model. See:
* {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#convert}.
* @returns {module:engine/model/documentfragment~DocumentFragment} Parsed data.
*/
parse( data, batch, context = '$root' ) {
parse( data, context = '$root' ) {
// data -> view
const viewDocumentFragment = this.processor.toView( data );

// view -> model
return this.toModel( viewDocumentFragment, batch, context );
return this.toModel( viewDocumentFragment, context );
}

/**
Expand All @@ -232,13 +228,12 @@ export default class DataController {
*
* @param {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment} viewElementOrFragment
* Element or document fragment which content will be converted.
* @param {module:engine/model/batch~Batch} batch Batch to which the deltas will be added.
* @param {String} [context='$root'] Base context in which the view will be converted to the model. See:
* {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#convert}.
* @returns {module:engine/model/documentfragment~DocumentFragment} Output document fragment.
*/
toModel( viewElementOrFragment, batch, context = '$root' ) {
return this.viewToModel.convert( viewElementOrFragment, batch, { context: [ context ] } );
toModel( viewElementOrFragment, context = '$root' ) {
return this.viewToModel.convert( viewElementOrFragment, { context: [ context ] } );
}

/**
Expand All @@ -252,11 +247,9 @@ export default class DataController {
* @fires insertContent
* @param {module:engine/model/documentfragment~DocumentFragment|module:engine/model/item~Item} content The content to insert.
* @param {module:engine/model/selection~Selection} selection Selection into which the content should be inserted.
* @param {module:engine/model/batch~Batch} [batch] Batch to which deltas will be added. If not specified, then
* changes will be added to a new batch.
*/
insertContent( content, selection, batch ) {
insertContent( this, content, selection, batch );
insertContent( content, selection ) {
insertContent( this, content, selection );
}

/**
Expand All @@ -272,11 +265,10 @@ export default class DataController {
*
* @fires deleteContent
* @param {module:engine/model/selection~Selection} selection Selection of which the content should be deleted.
* @param {module:engine/model/batch~Batch} batch Batch to which deltas will be added.
* @param {Object} options See {@link module:engine/controller/deletecontent~deleteContent}'s options.
*/
deleteContent( selection, batch, options ) {
deleteContent( selection, batch, options );
deleteContent( selection, options ) {
deleteContent( this, selection, options );
}

/**
Expand All @@ -295,11 +287,10 @@ export default class DataController {
*
* @fires module:engine/controller/datacontroller~DataController#getSelectedContent
* @param {module:engine/model/selection~Selection} selection The selection of which content will be retrieved.
* @param {module:engine/model/batch~Batch} batch Batch to which deltas will be added.
* @returns {module:engine/model/documentfragment~DocumentFragment} Document fragment holding the clone of the selected content.
*/
getSelectedContent( selection, batch ) {
return getSelectedContent( selection, batch );
getSelectedContent( selection ) {
return getSelectedContent( this, selection );
}

/**
Expand Down
112 changes: 57 additions & 55 deletions src/controller/deletecontent.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@
import LivePosition from '../model/liveposition';
import Position from '../model/position';
import Range from '../model/range';
import Element from '../model/element';

/**
* Deletes content of the selection and merge siblings. The resulting selection is always collapsed.
*
* @param {module:engine/controller/datacontroller~DataController} dataController The data controller in context of which the insertion
* should be performed.
* @param {module:engine/model/selection~Selection} selection Selection of which the content should be deleted.
* @param {module:engine/model/batch~Batch} batch Batch to which the deltas will be added.
* @param {Object} [options]
Expand All @@ -36,63 +37,65 @@ import Element from '../model/element';
* * `<paragraph>^</paragraph>` with the option disabled (`doNotResetEntireContent == false`)
* * `<heading>^</heading>` with enabled (`doNotResetEntireContent == true`).
*/
export default function deleteContent( selection, batch, options = {} ) {
export default function deleteContent( dataController, selection, options = {} ) {
if ( selection.isCollapsed ) {
return;
}

const schema = batch.document.schema;
const schema = dataController.model.schema;

// 1. Replace the entire content with paragraph.
// See: https://github.com/ckeditor/ckeditor5-engine/issues/1012#issuecomment-315017594.
if ( !options.doNotResetEntireContent && shouldEntireContentBeReplacedWithParagraph( schema, selection ) ) {
replaceEntireContentWithParagraph( batch, selection );
dataController.model.change( writer => {
// 1. Replace the entire content with paragraph.
// See: https://github.com/ckeditor/ckeditor5-engine/issues/1012#issuecomment-315017594.
if ( !options.doNotResetEntireContent && shouldEntireContentBeReplacedWithParagraph( schema, selection ) ) {
replaceEntireContentWithParagraph( writer, selection, schema );

return;
}
return;
}

const selRange = selection.getFirstRange();
const startPos = selRange.start;
const endPos = LivePosition.createFromPosition( selRange.end );
const selRange = selection.getFirstRange();
const startPos = selRange.start;
const endPos = LivePosition.createFromPosition( selRange.end );

// 2. Remove the content if there is any.
if ( !selRange.start.isTouching( selRange.end ) ) {
batch.remove( selRange );
}
// 2. Remove the content if there is any.
if ( !selRange.start.isTouching( selRange.end ) ) {
writer.remove( selRange );
}

// 3. Merge elements in the right branch to the elements in the left branch.
// The only reasonable (in terms of data and selection correctness) case in which we need to do that is:
//
// <heading type=1>Fo[</heading><paragraph>]ar</paragraph> => <heading type=1>Fo^ar</heading>
//
// However, the algorithm supports also merging deeper structures (up to the depth of the shallower branch),
// as it's hard to imagine what should actually be the default behavior. Usually, specific features will
// want to override that behavior anyway.
if ( !options.leaveUnmerged ) {
mergeBranches( batch, startPos, endPos );

// We need to check and strip disallowed attributes in all nested nodes because after merge
// some attributes could end up in a path where are disallowed.
// 3. Merge elements in the right branch to the elements in the left branch.
// The only reasonable (in terms of data and selection correctness) case in which we need to do that is:
//
// e.g. bold is disallowed for <H1>
// <h1>Fo{o</h1><p>b}a<b>r</b><p> -> <h1>Fo{}a<b>r</b><h1> -> <h1>Fo{}ar<h1>.
schema.removeDisallowedAttributes( startPos.parent.getChildren(), startPos, batch );
}
// <heading type=1>Fo[</heading><paragraph>]ar</paragraph> => <heading type=1>Fo^ar</heading>
//
// However, the algorithm supports also merging deeper structures (up to the depth of the shallower branch),
// as it's hard to imagine what should actually be the default behavior. Usually, specific features will
// want to override that behavior anyway.
if ( !options.leaveUnmerged ) {
mergeBranches( writer, startPos, endPos );

// We need to check and strip disallowed attributes in all nested nodes because after merge
// some attributes could end up in a path where are disallowed.
//
// e.g. bold is disallowed for <H1>
// <h1>Fo{o</h1><p>b}a<b>r</b><p> -> <h1>Fo{}a<b>r</b><h1> -> <h1>Fo{}ar<h1>.
schema.removeDisallowedAttributes( startPos.parent.getChildren(), startPos, writer );
}

selection.setCollapsedAt( startPos );
selection.setCollapsedAt( startPos );

// 4. Autoparagraphing.
// Check if a text is allowed in the new container. If not, try to create a new paragraph (if it's allowed here).
if ( shouldAutoparagraph( schema, startPos ) ) {
insertParagraph( batch, startPos, selection );
}
// 4. Autoparagraphing.
// Check if a text is allowed in the new container. If not, try to create a new paragraph (if it's allowed here).
if ( shouldAutoparagraph( schema, startPos ) ) {
insertParagraph( writer, startPos, selection );
}

endPos.detach();
endPos.detach();
} );
}

// This function is a result of reaching the Ballmer's peak for just the right amount of time.
// Even I had troubles documenting it after a while and after reading it again I couldn't believe that it really works.
function mergeBranches( batch, startPos, endPos ) {
function mergeBranches( writer, startPos, endPos ) {
const startParent = startPos.parent;
const endParent = endPos.parent;

Expand All @@ -112,7 +115,7 @@ function mergeBranches( batch, startPos, endPos ) {
// Check if operations we'll need to do won't need to cross object or limit boundaries.
// E.g., we can't merge endParent into startParent in this case:
// <limit><startParent>x[]</startParent></limit><endParent>{}</endParent>
if ( !checkCanBeMerged( startPos, endPos ) ) {
if ( !checkCanBeMerged( startPos, endPos, writer.model.schema ) ) {
return;
}

Expand All @@ -128,13 +131,13 @@ function mergeBranches( batch, startPos, endPos ) {
// <a><b>x[]</b></a><c><d>{}y</d></c>
// becomes:
// <a><b>x</b>[]<d>y</d></a><c>{}</c>
batch.insert( endParent, startPos );
writer.insert( endParent, startPos );
}

// Merge two siblings:
// <a>x</a>[]<b>y</b> -> <a>xy</a> (the usual case)
// <a><b>x</b>[]<d>y</d></a><c></c> -> <a><b>xy</b>[]</a><c></c> (this is the "move parent" case shown above)
batch.merge( startPos );
writer.merge( startPos );

// Remove empty end ancestors:
// <a>fo[o</a><b><a><c>bar]</c></a></b>
Expand All @@ -146,11 +149,11 @@ function mergeBranches( batch, startPos, endPos ) {

endPos = Position.createBefore( parentToRemove );

batch.remove( parentToRemove );
writer.remove( parentToRemove );
}

// Continue merging next level.
mergeBranches( batch, startPos, endPos );
mergeBranches( writer, startPos, endPos );
}

function shouldAutoparagraph( schema, position ) {
Expand All @@ -166,8 +169,7 @@ function shouldAutoparagraph( schema, position ) {
// E.g. in <bQ><p>x[]</p></bQ><widget><caption>{}</caption></widget>
// we'll check <p>, <bQ>, <widget> and <caption>.
// Usually, widget and caption are marked as objects/limits in the schema, so in this case merging will be blocked.
function checkCanBeMerged( leftPos, rightPos ) {
const schema = leftPos.root.document.schema;
function checkCanBeMerged( leftPos, rightPos, schema ) {
const rangeToCheck = new Range( leftPos, rightPos );

for ( const value of rangeToCheck.getWalker() ) {
Expand All @@ -179,18 +181,18 @@ function checkCanBeMerged( leftPos, rightPos ) {
return true;
}

function insertParagraph( batch, position, selection ) {
const paragraph = new Element( 'paragraph' );
batch.insert( paragraph, position );
function insertParagraph( writer, position, selection ) {
const paragraph = writer.createElement( 'paragraph' );

writer.insert( paragraph, position );
selection.setCollapsedAt( paragraph );
}

function replaceEntireContentWithParagraph( batch, selection ) {
const limitElement = batch.document.schema.getLimitElement( selection );
function replaceEntireContentWithParagraph( writer, selection ) {
const limitElement = writer.model.schema.getLimitElement( selection );

batch.remove( Range.createIn( limitElement ) );
insertParagraph( batch, Position.createAt( limitElement ), selection );
writer.remove( Range.createIn( limitElement ) );
insertParagraph( writer, Position.createAt( limitElement ), selection );
}

// We want to replace the entire content with a paragraph when:
Expand Down
14 changes: 7 additions & 7 deletions src/controller/editingcontroller.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,14 @@ export default class EditingController {
/**
* Creates editing controller instance.
*
* @param {module:engine/model/document~Document} model Document model.
* @param {module:engine/model/model~Model} model Editing model.
*/
constructor( model ) {
/**
* Document model.
* Editing model.
*
* @readonly
* @member {module:engine/model/document~Document}
* @member {module:engine/model/model~Model}
*/
this.model = model;

Expand Down Expand Up @@ -84,13 +84,13 @@ export default class EditingController {
} );

// Convert changes in model to view.
this.listenTo( this.model, 'change', ( evt, type, changes ) => {
this.listenTo( this.model.document, 'change', ( evt, type, changes ) => {
this.modelToView.convertChange( type, changes );
}, { priority: 'low' } );

// Convert model selection to view.
this.listenTo( this.model, 'changesDone', () => {
const selection = this.model.selection;
this.listenTo( this.model.document, 'changesDone', () => {
const selection = this.model.document.selection;

this.modelToView.convertSelection( selection );
this.view.render();
Expand Down Expand Up @@ -139,7 +139,7 @@ export default class EditingController {
*/
createRoot( domRoot, name = 'main' ) {
const viewRoot = this.view.createRoot( domRoot, name );
const modelRoot = this.model.getRoot( name );
const modelRoot = this.model.document.getRoot( name );

this.mapper.bindElements( modelRoot, viewRoot );

Expand Down
Loading

0 comments on commit 5be1ad6

Please # to comment.