/** * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md. */ 'use strict'; import ModelPosition from '../treemodel/position.js'; import ViewPosition from '../treeview/position.js'; import ViewText from '../treeview/text.js'; /** * Maps elements and positions between {@link core.treeView.TreeView TreeView} and {@link core.treeModel TreeModel}. * * Mapper use binded elements to find corresponding elements and positions, so, to get proper results, * all Tree Model elements should be {@link core.treeController.Mapper#bindElements binded}. * * @memberOf core.treeController */ export default class Mapper { /** * Creates an instance of the mapper. */ constructor() { /** * Model element to View element mapping. * * @private * @member {WeakMap} core.treeController.Mapper#_modelToViewMapping */ this._modelToViewMapping = new WeakMap(); /** * View element to Model element mapping. * * @private * @member {WeakMap} core.treeController.Mapper#_viewToModelMapping */ this._viewToModelMapping = new WeakMap(); } /** * Marks model and view elements as corresponding. Corresponding elements can be retrieved by using * the {@link core.treeController.Mapper#toModelElement toModelElement} and {@link core.treeController.Mapper#toViewElement toViewElement} * methods. The information that elements are bound is also used to translate positions. * * @param {core.treeModel.Element} modelElement Model element. * @param {core.treeView.Element} viewElement View element. */ bindElements( modelElement, viewElement ) { this._modelToViewMapping.set( modelElement, viewElement ); this._viewToModelMapping.set( viewElement, modelElement ); } /** * Gets the corresponding model element. * * @param {core.treeView.Element} viewElement View element. * @returns {core.treeModel.Element|null} Corresponding model element or `null` if not found. */ toModelElement( viewElement ) { return this._viewToModelMapping.get( viewElement ); } /** * Gets the corresponding view element. * * @param {core.treeModel.Element} modelElement Model element. * @returns {core.treeView.Element|null} Corresponding view element or `null` if not found. */ toViewElement( modelElement ) { return this._modelToViewMapping.get( modelElement ); } /** * Gets the corresponding model position. * * @param {core.treeView.Position} viewPosition View position. * @returns {core.treeModel.Position} Corresponding model position. */ toModelPosition( viewPosition ) { let viewBlock = viewPosition.parent; let modelParent = this._viewToModelMapping.get( viewBlock ); while ( !modelParent ) { viewBlock = viewBlock.parent; modelParent = this._viewToModelMapping.get( viewBlock ); } let modelOffset = this._toModelOffset( viewPosition.parent, viewPosition.offset, viewBlock ); return ModelPosition.createFromParentAndOffset( modelParent, modelOffset ); } /** * Gets the corresponding view position. * * @param {core.treeModel.Position} modelPosition Model position. * @returns {core.treeView.Position} Corresponding view position. */ toViewPosition( modelPosition ) { let viewContainer = this._modelToViewMapping.get( modelPosition.parent ); return this._findPositionIn( viewContainer, modelPosition.offset ); } /** * Calculates model offset based on the view position and the block element. * * Example: * *

fooba|r

// _toModelOffset( b, 2, p ) -> 5 * * Is a sum of: * *

foo|bar

// _toModelOffset( p, 3, p ) -> 3 *

fooba|r

// _toModelOffset( b, 2, b ) -> 2 * * @private * @param {core.treeView.Element} viewParent Position parent. * @param {Number} viewOffset Position offset. * @param {core.treeView.Element} viewBlock Block used as a base to calculate offset. * @returns {Number} Offset in the model. */ _toModelOffset( viewParent, viewOffset, viewBlock ) { if ( viewBlock != viewParent ) { // See example. const offsetToParentBegging = this._toModelOffset( viewParent.parent, viewParent.getIndex(), viewBlock ); const offsetInParent = this._toModelOffset( viewParent, viewOffset, viewParent ); return offsetToParentBegging + offsetInParent; } // viewBlock == viewParent, so we need to calculate the offset in the parent element. // If the position is a text it is simple ("ba|r" -> 2). if ( viewParent instanceof ViewText ) { return viewOffset; } // If the position is in an element we need to sum lengths of siblings ( bar foo | -> 3 + 3 = 6 ). let modelOffset = 0; for ( let i = 0; i < viewOffset; i++ ) { modelOffset += this._getModelLength( viewParent.getChild( i ) ); } return modelOffset; } /** * Gets the length of the view element in the model. * * Examples: * * foo -> 3 // Length of the text is the length of data. * foo -> 3 // Length the element which has no corresponding model element is a length of its children. *

foo

-> 1 // Length the element which has corresponding model element is always 1. * * @private * @param {core.treeView.Element} viewNode View node. * @returns {Number} Length of the node in the tree model. */ _getModelLength( viewNode ) { // If we need mapping to be more flexible this method may fire event, so every feature may define the relation // between length in the model to the length in the view, for example if one element in the model creates two // elements in the view. Now I can not find any example where such feature would be useful. if ( this._viewToModelMapping.has( viewNode ) ) { return 1; } else if ( viewNode instanceof ViewText ) { return viewNode.data.length; } else { let len = 0; for ( let child of viewNode.getChildren() ) { len += this._getModelLength( child ); } return len; } } /** * Finds the position in the view node (or its children) with the expected model offset. * * Example: * *

fobarbom

-> expected offset: 4 * * _findPositionIn( p, 4 ): *

|fobarbom

-> expected offset: 4, actual offset: 0 *

fo|barbom

-> expected offset: 4, actual offset: 2 *

fobar|bom

-> expected offset: 4, actual offset: 5 -> we are too far * * _findPositionIn( b, 4 - ( 5 - 3 ) ): *

fo|barbom

-> expected offset: 2, actual offset: 0 *

fobar|bom

-> expected offset: 2, actual offset: 3 -> we are too far * * _findPositionIn( bar, 2 - ( 3 - 3 ) ): * We are in the text node so we can simple find the offset. *

foba|rbom

-> expected offset: 2, actual offset: 2 -> position found * * @private * @param {core.treeView.Element} viewParent Tree view element in which we are looking for the position. * @param {Number} expectedOffset Expected offset. * @returns {core.treeView.Position} Found position. */ _findPositionIn( viewParent, expectedOffset ) { // Last scanned view node. let viewNode; // Length of the last scanned view node. let lastLength = 0; let modelOffset = 0; let viewOffset = 0; // In the text node it is simple: offset in the model equals offset in the text. if ( viewParent instanceof ViewText ) { return new ViewPosition( viewParent, expectedOffset ); } // In other cases we add lengths of child nodes to find the proper offset. // If it is smaller we add the length. while ( modelOffset < expectedOffset ) { viewNode = viewParent.getChild( viewOffset ); lastLength = this._getModelLength( viewNode ); modelOffset += lastLength; viewOffset++; } // If it equals we found the position. if ( modelOffset == expectedOffset ) { return new ViewPosition( viewParent, viewOffset ); } // If it is higher we need to enter last child. else { // ( modelOffset - lastLength ) is the offset to the child we enter, // so we subtract it from the expected offset to fine the offset in the child. return this._findPositionIn( viewNode, expectedOffset - ( modelOffset - lastLength ) ); } } }