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

Bound view roots to model roots. #1233

Merged
merged 15 commits into from
Jan 11, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 16 additions & 24 deletions src/controller/editingcontroller.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* @module engine/controller/editingcontroller
*/

import RootEditableElement from '../view/rooteditableelement';
import ModelDiffer from '../model/differ';
import ViewDocument from '../view/document';
import Mapper from '../conversion/mapper';
Expand Down Expand Up @@ -141,33 +142,24 @@ export default class EditingController {
this.modelToView.on( 'selection', clearFakeSelection(), { priority: 'low' } );
this.modelToView.on( 'selection', convertRangeSelection(), { priority: 'low' } );
this.modelToView.on( 'selection', convertCollapsedSelection(), { priority: 'low' } );
}

/**
* {@link module:engine/view/document~Document#createRoot Creates} a view root
* and {@link module:engine/conversion/mapper~Mapper#bindElements binds}
* the model root with view root and and view root with DOM element:
*
* editing.createRoot( document.querySelector( div#editor ) );
*
* If the DOM element is not available at the time you want to create a view root, for instance it is iframe body
* element, it is possible to create view element and bind the DOM element later:
*
* editing.createRoot( 'body' );
* editing.view.attachDomRoot( iframe.contentDocument.body );
*
* @param {Element|String} domRoot DOM root element or the name of view root element if the DOM element will be
* attached later.
* @param {String} [name='main'] Root name.
* @returns {module:engine/view/containerelement~ContainerElement} View root element.
*/
createRoot( domRoot, name = 'main' ) {
const viewRoot = this.view.createRoot( domRoot, name );
const modelRoot = this.model.document.getRoot( name );
// Binds {@link module:engine/view/document~Document#roots view roots collection} to
// {@link module:engine/model/document~Document#roots model roots collection} so creating
// model root automatically creates corresponding view root.
this.view.roots.bindTo( this.model.document.roots ).using( root => {
// $graveyard is a special root that has no reflection in the view.
if ( root.rootName == '$graveyard' ) {
return null;
}

this.mapper.bindElements( modelRoot, viewRoot );
const viewRoot = new RootEditableElement( root.name );

return viewRoot;
viewRoot.rootName = root.rootName;
viewRoot.document = this.view;
this.mapper.bindElements( root, viewRoot );

return viewRoot;
} );
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/dev-utils/enableenginedebug.js
Original file line number Diff line number Diff line change
Expand Up @@ -679,7 +679,7 @@ class DebugPlugin extends Plugin {
function dumpTrees( document, version ) {
let string = '';

for ( const root of document.roots.values() ) {
for ( const root of document.roots ) {
string += root.printTree() + '\n';
}

Expand Down
39 changes: 9 additions & 30 deletions src/model/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import RootElement from './rootelement';
import History from './history';
import DocumentSelection from './documentselection';
import TreeWalker from './treewalker';
import Collection from '@ckeditor/ckeditor5-utils/src/collection';
import clone from '@ckeditor/ckeditor5-utils/src/lib/lodash/clone';
import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
Expand Down Expand Up @@ -76,9 +77,9 @@ export default class Document {
* {@link #getRoot} to manipulate it.
*
* @readonly
* @member {Map}
* @member {module:utils/collection~Collection}
*/
this.roots = new Map();
this.roots = new Collection( { idProperty: 'rootName' } );

// Add events that will ensure selection correctness.
this.selection.on( 'change:range', () => {
Expand Down Expand Up @@ -155,7 +156,7 @@ export default class Document {
* @returns {module:engine/model/rootelement~RootElement} Created root.
*/
createRoot( elementName = '$root', rootName = 'main' ) {
if ( this.roots.has( rootName ) ) {
if ( this.roots.get( rootName ) ) {
/**
* Root with specified name already exists.
*
Expand All @@ -170,7 +171,7 @@ export default class Document {
}

const root = new RootElement( this, elementName, rootName );
this.roots.set( rootName, root );
this.roots.add( root );

return root;
}
Expand All @@ -187,42 +188,20 @@ export default class Document {
* Returns top-level root by its name.
*
* @param {String} [name='main'] Unique root name.
* @returns {module:engine/model/rootelement~RootElement} Root registered under given name.
* @returns {module:engine/model/rootelement~RootElement|null} Root registered under given name or null when
* there is no root of given name.
*/
getRoot( name = 'main' ) {
if ( !this.roots.has( name ) ) {
/**
* Root with specified name does not exist.
*
* @error model-document-getRoot-root-not-exist
* @param {String} name
*/
throw new CKEditorError(
'model-document-getRoot-root-not-exist: Root with specified name does not exist.',
{ name }
);
}

return this.roots.get( name );
}

/**
* Checks if root with given name is defined.
*
* @param {String} name Name of root to check.
* @returns {Boolean}
*/
hasRoot( name ) {
return this.roots.has( name );
}

/**
* Returns array with names of all roots (without the {@link #graveyard}) added to the document.
*
* @returns {Array.<String>} Roots names.
*/
getRootNames() {
return Array.from( this.roots.keys() ).filter( name => name != graveyardName );
return Array.from( this.roots, root => root.rootName ).filter( name => name != graveyardName );
}

/**
Expand Down Expand Up @@ -301,7 +280,7 @@ export default class Document {
* @returns {module:engine/model/rootelement~RootElement} The default root for this document.
*/
_getDefaultRoot() {
for ( const root of this.roots.values() ) {
for ( const root of this.roots ) {
if ( root !== this.graveyard ) {
return root;
}
Expand Down
2 changes: 1 addition & 1 deletion src/model/operation/rootattributeoperation.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ export default class RootAttributeOperation extends Operation {
* @returns {module:engine/model/operation/rootattributeoperation~RootAttributeOperation}
*/
static fromJSON( json, document ) {
if ( !document.hasRoot( json.root ) ) {
if ( !document.getRoot( json.root ) ) {
/**
* Cannot create RootAttributeOperation for document. Root with specified name does not exist.
*
Expand Down
2 changes: 1 addition & 1 deletion src/model/position.js
Original file line number Diff line number Diff line change
Expand Up @@ -764,7 +764,7 @@ export default class Position {
return new Position( doc.graveyard, json.path );
}

if ( !doc.hasRoot( json.root ) ) {
if ( !doc.getRoot( json.root ) ) {
/**
* Cannot create position for document. Root with specified name does not exist.
*
Expand Down
80 changes: 24 additions & 56 deletions src/view/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
import Selection from './selection';
import Renderer from './renderer';
import DomConverter from './domconverter';
import RootEditableElement from './rooteditableelement';
import { injectQuirksHandling } from './filler';
import { injectUiElementHandling } from './uielement';
import log from '@ckeditor/ckeditor5-utils/src/log';
Expand All @@ -19,6 +18,7 @@ import SelectionObserver from './observer/selectionobserver';
import FocusObserver from './observer/focusobserver';
import KeyObserver from './observer/keyobserver';
import FakeSelectionObserver from './observer/fakeselectionobserver';
import Collection from '@ckeditor/ckeditor5-utils/src/collection';
import mix from '@ckeditor/ckeditor5-utils/src/mix';
import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
import { scrollViewportToShowTarget } from '@ckeditor/ckeditor5-utils/src/dom/scroll';
Expand Down Expand Up @@ -74,12 +74,17 @@ export default class Document {
this.domConverter = new DomConverter();

/**
* Roots of the view tree. Map of the {module:engine/view/element~Element view elements} with roots names as keys.
* Roots of the view tree. Collection of the {module:engine/view/element~Element view elements}.
*
* View roots are created as a result of binding between {@link module:engine/view/document~Document#roots} and
* {@link module:engine/model/document~Document#roots} and this is handled by
* {@link module:engine/controller/editingcontroller~EditingController}, so to create view root we need to create
* model root using {@link module:engine/model/document~Document#createRoot}.
*
* @readonly
* @member {Map} module:engine/view/document~Document#roots
* @member {Collection} module:engine/view/document~Document#roots
*/
this.roots = new Map();
this.roots = new Collection( { idProperty: 'rootName' } );

/**
* Defines whether document is in read-only mode.
Expand Down Expand Up @@ -177,69 +182,31 @@ export default class Document {
}

/**
* Creates a {@link module:engine/view/document~Document#roots view root element}.
*
* If the DOM element is passed as a first parameter it will be automatically
* {@link module:engine/view/document~Document#attachDomRoot attached}:
*
* document.createRoot( document.querySelector( 'div#editor' ) ); // Will call document.attachDomRoot.
*
* However, if the string is passed, then only the view element will be created and the DOM element have to be
* attached separately:
*
* document.createRoot( 'body' );
* document.attachDomRoot( document.querySelector( 'body#editor' ) );
*
* In both cases, {@link module:engine/view/rooteditableelement~RootEditableElement#rootName element name} is always
* transformed to lower
* case.
*
* @param {Element|String} domRoot DOM root element or the tag name of view root element if the DOM element will be
* attached later.
* @param {String} [name='main'] Name of the root.
* @returns {module:engine/view/rooteditableelement~RootEditableElement} The created view root element.
*/
createRoot( domRoot, name = 'main' ) {
const rootTag = typeof domRoot == 'string' ? domRoot : domRoot.tagName;

const viewRoot = new RootEditableElement( rootTag.toLowerCase(), name );
viewRoot.document = this;

this.roots.set( name, viewRoot );

// Mark changed nodes in the renderer.
viewRoot.on( 'change:children', ( evt, node ) => this.renderer.markToSync( 'children', node ) );
viewRoot.on( 'change:attributes', ( evt, node ) => this.renderer.markToSync( 'attributes', node ) );
viewRoot.on( 'change:text', ( evt, node ) => this.renderer.markToSync( 'text', node ) );

if ( this.domConverter.isElement( domRoot ) ) {
this.attachDomRoot( domRoot, name );
}

return viewRoot;
}

/**
* Attaches DOM root element to the view element and enable all observers on that element. This method also
* {@link module:engine/view/renderer~Renderer#markToSync mark element} to be synchronized with the view what means that all child
* nodes will be removed and replaced with content of the view root.
* Attaches DOM root element to the view element and enable all observers on that element.
* Also {@link module:engine/view/renderer~Renderer#markToSync mark element} to be synchronized with the view
* what means that all child nodes will be removed and replaced with content of the view root.
*
* Note that {@link module:engine/view/document~Document#createRoot} will call this method automatically if the DOM element is
* passed to it.
* This method also will change view element name as the same as tag name of given dom root.
* Name is always transformed to lower case.
*
* @param {Element|String} domRoot DOM root element.
* @param {Element} domRoot DOM root element.
* @param {String} [name='main'] Name of the root.
*/
attachDomRoot( domRoot, name = 'main' ) {
const viewRoot = this.getRoot( name );

this.domRoots.set( name, domRoot );
// Set view root name the same as DOM root tag name.
viewRoot._name = domRoot.tagName.toLowerCase();

this.domRoots.set( name, domRoot );
this.domConverter.bindElements( domRoot, viewRoot );

this.renderer.markToSync( 'children', viewRoot );
this.renderer.domDocuments.add( domRoot.ownerDocument );

viewRoot.on( 'change:children', ( evt, node ) => this.renderer.markToSync( 'children', node ) );
viewRoot.on( 'change:attributes', ( evt, node ) => this.renderer.markToSync( 'attributes', node ) );
viewRoot.on( 'change:text', ( evt, node ) => this.renderer.markToSync( 'text', node ) );

for ( const observer of this._observers.values() ) {
observer.observe( domRoot, name );
}
Expand All @@ -250,7 +217,8 @@ export default class Document {
* specific "main" root is returned.
*
* @param {String} [name='main'] Name of the root.
* @returns {module:engine/view/rooteditableelement~RootEditableElement} The view root element with the specified name.
* @returns {module:engine/view/rooteditableelement~RootEditableElement|null} The view root element with the specified name
* or null when there is no root of given name.
*/
getRoot( name = 'main' ) {
return this.roots.get( name );
Expand Down
14 changes: 13 additions & 1 deletion src/view/rooteditableelement.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ export default class RootEditableElement extends EditableElement {
/**
* Creates root editable element.
*
* @param {module:engine/view/document~Document} document {@link module:engine/view/document~Document} that is an owner of the root.
* @param {String} name Node name.
*/
constructor( name ) {
Expand Down Expand Up @@ -56,4 +55,17 @@ export default class RootEditableElement extends EditableElement {
set rootName( rootName ) {
this.setCustomProperty( rootNameSymbol, rootName );
}

/**
* Overrides old element name and sets new one.
* This is needed because view roots are created before they are attached to the DOM.
* The name of the root element is temporary at this stage. It has to be changed when the
* view root element is attached to the DOM element.
*
* @protected
* @param {String} name The new name of element.
*/
set _name( name ) {
this.name = name;
}
}
Loading