_getTransformedByInsertion( insertPosition, howMany ) {
const transformed = Position._createAt( this );
// This position can't be affected if insertion was in a different root.if ( this.root != insertPosition.root ) {
return transformed;
}
if ( compareArrays( insertPosition.getParentPath(), this.getParentPath() ) == 'same' ) {
// If nodes are inserted in the node that is pointed by this position...if ( insertPosition.offset < this.offset || ( insertPosition.offset == this.offset && this.stickiness != 'toPrevious' ) ) {
// And are inserted before an offset of that position...// "Push" this positions offset.
transformed.offset += howMany;
}
} elseif ( compareArrays( insertPosition.getParentPath(), this.getParentPath() ) == 'prefix' ) {
// If nodes are inserted in a node that is on a path to this position...const i = insertPosition.path.length - 1;
if ( insertPosition.offset <= this.path[ i ] ) {
// And are inserted before next node of that path...// "Push" the index on that path.
transformed.path[ i ] += howMany;
}
}
return transformed;
}
//第一种情况// should increment offset if insertion is in the same parent and the same offsetconst position = newPosition( root, [ 1, 2, 3 ] );
position.stickiness = 'toNext';
const transformed = position._getTransformedByInsertion( newPosition( root, [ 1, 2, 3 ] ), 2 );
expect( transformed.offset ).to.equal( 5 );
//第二种情况//should increment offset if insertion is in the same parent and closer offsetconst position = newPosition( root, [ 1, 2, 3 ] );
const transformed = position._getTransformedByInsertion( newPosition( root, [ 1, 2, 2 ] ), 2 );
expect( transformed.offset ).to.equal( 5 );
3、如果插入位置的parentPath在当前位置的parentPath之前,那么这种情况如下:
//should update path if insertion position parent is a node from that path and offset is before next node on that pathconst position = newPosition( root, [ 1, 2, 3 ] );
const transformed = position._getTransformedByInsertion( newPosition( root, [ 1, 2 ] ), 2 );
expect( transformed.path ).to.deep.equal( [ 1, 4, 3 ] );
exportdefaultfunctioncompareArrays( a, b ) {
const minLen = Math.min( a.length, b.length );
for ( let i = 0; i < minLen; i++ ) {
if ( a[ i ] != b[ i ] ) {
// The arrays are different.return i;
}
}
// Both arrays were same at all points.if ( a.length == b.length ) {
// If their length is also same, they are the same.return'same';
} elseif ( a.length < b.length ) {
// Compared array is shorter so it is a prefix of the other array.return'prefix';
} else {
// Compared array is longer so it is an extension of the other array.return'extension';
}
}
exportdefaultclassPosition {
/**
* Creates a position.
*
* @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} root Root of the position.
* @param {Array.<Number>} path Position path. See {@link module:engine/model/position~Position#path}.
* @param {module:engine/model/position~PositionStickiness} [stickiness='toNone'] Position stickiness.
* See {@link module:engine/model/position~PositionStickiness}.
*/constructor( root, path, stickiness = 'toNone' ) {
if ( !root.is( 'element' ) && !root.is( 'documentFragment' ) ) {
/**
* Position root is invalid.
*
* Positions can only be anchored in elements or document fragments.
*
* @error model-position-root-invalid
*/thrownewCKEditorError(
'model-position-root-invalid',
root
);
}
if ( !( path instanceofArray ) || path.length === 0 ) {
/**
* Position path must be an array with at least one item.
*
* @error model-position-path-incorrect-format
* @param path
*/thrownewCKEditorError(
'model-position-path-incorrect-format',
root,
{ path }
);
}
// Normalize the root and path when element (not root) is passed.if ( root.is( 'rootElement' ) ) {
path = path.slice();
} else {
path = [ ...root.getPath(), ...path ];
root = root.root;
}
/**
* Root of the position path.
*
* @readonly
* @member {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment}
* module:engine/model/position~Position#root
*/this.root = root;
/**
* Position of the node in the tree. **Path contains offsets, not indexes.**
*
* Position can be placed before, after or in a {@link module:engine/model/node~Node node} if that node has
* {@link module:engine/model/node~Node#offsetSize} greater than `1`. Items in position path are
* {@link module:engine/model/node~Node#startOffset starting offsets} of position ancestors, starting from direct root children,
* down to the position offset in it's parent.
*
* ROOT
* |- P before: [ 0 ] after: [ 1 ]
* |- UL before: [ 1 ] after: [ 2 ]
* |- LI before: [ 1, 0 ] after: [ 1, 1 ]
* | |- foo before: [ 1, 0, 0 ] after: [ 1, 0, 3 ]
* |- LI before: [ 1, 1 ] after: [ 1, 2 ]
* |- bar before: [ 1, 1, 0 ] after: [ 1, 1, 3 ]
*
* `foo` and `bar` are representing {@link module:engine/model/text~Text text nodes}. Since text nodes has offset size
* greater than `1` you can place position offset between their start and end:
*
* ROOT
* |- P
* |- UL
* |- LI
* | |- f^o|o ^ has path: [ 1, 0, 1 ] | has path: [ 1, 0, 2 ]
* |- LI
* |- b^a|r ^ has path: [ 1, 1, 1 ] | has path: [ 1, 1, 2 ]
*
* @readonly
* @member {Array.<Number>} module:engine/model/position~Position#path
*/this.path = path;
/**
* Position stickiness. See {@link module:engine/model/position~PositionStickiness}.
*
* @member {module:engine/model/position~PositionStickiness} module:engine/model/position~Position#stickiness
*/this.stickiness = stickiness;
}
}
getparent() {
let parent = this.root;
for ( let i = 0; i < this.path.length - 1; i++ ) {
parent = parent.getChild( parent.offsetToIndex( this.path[ i ] ) );
if ( !parent ) {
/**
* The position's path is incorrect. This means that a position does not point to
* a correct place in the tree and hence, some of its methods and getters cannot work correctly.
*
* **Note**: Unlike DOM and view positions, in the model, the
* {@link module:engine/model/position~Position#parent position's parent} is always an element or a document fragment.
* The last offset in the {@link module:engine/model/position~Position#path position's path} is the point in this element
* where this position points.
*
* Read more about model positions and offsets in
* the {@glink framework/guides/architecture/editing-engine#indexes-and-offsets Editing engine architecture guide}.
*
* @error model-position-path-incorrect
* @param {module:engine/model/position~Position} position The incorrect position.
*/thrownewCKEditorError( 'model-position-path-incorrect', this, { position: this } );
}
}
if ( parent.is( '$text' ) ) {
thrownewCKEditorError( 'model-position-path-incorrect', this, { position: this } );
}
return parent;
}
exportdefaultclassNode {
/**
* Creates a model node.
*
* This is an abstract class, so this constructor should not be used directly.
*
* @abstract
* @param {Object} [attrs] Node's attributes. See {@link module:utils/tomap~toMap} for a list of accepted values.
*/constructor( attrs ) {
/**
* Parent of this node. It could be {@link module:engine/model/element~Element}
* or {@link module:engine/model/documentfragment~DocumentFragment}.
* Equals to `null` if the node has no parent.
*
* @readonly
* @member {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment|null}
*/this.parent = null;
/**
* Attributes set on this node.
*
* @private
* @member {Map} module:engine/model/node~Node#_attrs
*/this._attrs = toMap( attrs );
}
}
export defaultclassElementextendsNode {
/**
* Creates a model element.
*
* **Note:** Constructor of this class shouldn't be used directly in the code.
* Use the {@link module:engine/model/writer~Writer#createElement} method instead.
*
* @protected
* @param {String} name Element's name.
* @param {Object} [attrs] Element's attributes. See {@link module:utils/tomap~toMap} for a list of accepted values.
* @param {module:engine/model/node~Node|Iterable.<module:engine/model/node~Node>} [children]
* One or more nodes to be inserted as children of created element.
*/
constructor( name, attrs, children ) {
super( attrs );
/**
* Element name.
*
* @readonly
* @member {String} module:engine/model/element~Element#name
*/this.name = name;
/**
* List of children nodes.
*
* @private
* @member {module:engine/model/nodelist~NodeList} module:engine/model/element~Element#_children
*/this._children = newNodeList();
if ( children ) {
this._insertChild( 0, children );
}
}
}
_renderNode( data ) {
let isInvalid;
if ( data.node ) {
// When applying, a definition cannot have "tag" and "text" at the same time.
isInvalid = this.tag && this.text;
} else {
// When rendering, a definition must have either "tag" or "text": XOR( this.tag, this.text ).
isInvalid = this.tag ? this.text : !this.text;
}
if ( isInvalid ) {
/**
* Node definition cannot have the "tag" and "text" properties at the same time.
* Node definition must have either "tag" or "text" when rendering a new Node.
*
* @error ui-template-wrong-syntax
*/thrownewCKEditorError(
'ui-template-wrong-syntax',
this
);
}
if ( this.text ) {
returnthis._renderText( data );
} else {
returnthis._renderElement( data );
}
}
从这个方法,我们可以看出,首先会判断节点的数据是否合法,这里有一个逻辑就是tag和text属性不能同时存在,如果同时存在就认为是不合法的,并且还有一点需要注意的是:这个方法会被render(data)和apply(data)调用,它们的判断逻辑是不一样的。剩下的逻辑就是如果当前的节点是text,那么就调用this._renderText( data ),否则调用this._renderElement( data )。
下面我们看看this._renderText( data )
_renderText( data ) {
let node = data.node;
// Save the original textContent to revert it in #revert().if ( node ) {
data.revertData.text = node.textContent;
} else {
node = data.node = document.createTextNode( '' );
}
// Check if this Text Node is bound to Observable. Cases://// text: [ Template.bind( ... ).to( ... ) ]//// text: [// 'foo',// Template.bind( ... ).to( ... ),// ...// ]//if ( hasTemplateBinding( this.text ) ) {
this._bindToObservable( {
schema: this.text,
updater: getTextUpdater( node ),
data
} );
}
// Simply set text. Cases://// text: [ 'all', 'are', 'static' ]//// text: [ 'foo' ]//else {
node.textContent = this.text.join( '' );
}
return node;
}
_bindToObservable( { schema, updater, data } ) {
const revertData = data.revertData;
// Set initial values.syncValueSchemaValue( schema, updater, data );
const revertBindings = schema
// Filter "falsy" (false, undefined, null, '') value schema components out.
.filter( item => !isFalsy( item ) )
// Filter inactive bindings from schema, like static strings ('foo'), numbers (42), etc.
.filter( item => item.observable )
// Once only the actual binding are left, let the emitter listen to observable change:attribute event.// TODO: Reduce the number of listeners attached as many bindings may listen// to the same observable attribute.
.map( templateBinding => templateBinding.activateAttributeListener( schema, updater, data ) );
if ( revertData ) {
revertData.bindings.push( revertBindings );
}
}
export classTemplateBinding {
/**
* Creates an instance of the {@link module:ui/template~TemplateBinding} class.
*
* @param {module:ui/template~TemplateDefinition} def The definition of the binding.
*/
constructor( def ) {
Object.assign( this, def );
/**
* An observable instance of the binding. It either:
*
* * provides the attribute with the value,
* * or passes the event when a corresponding DOM event is fired.
*
* @member {module:utils/observablemixin~ObservableMixin} module:ui/template~TemplateBinding#observable
*//**
* An {@link module:utils/emittermixin~Emitter} used by the binding to:
*
* * listen to the attribute change in the {@link module:ui/template~TemplateBinding#observable},
* * or listen to the event in the DOM.
*
* @member {module:utils/emittermixin~EmitterMixin} module:ui/template~TemplateBinding#emitter
*//**
* The name of the {@link module:ui/template~TemplateBinding#observable observed attribute}.
*
* @member {String} module:ui/template~TemplateBinding#attribute
*//**
* A custom function to process the value of the {@link module:ui/template~TemplateBinding#attribute}.
*
* @member {Function} [module:ui/template~TemplateBinding#callback]
*/
}
/**
* Returns the value of the binding. It is the value of the {@link module:ui/template~TemplateBinding#attribute} in
* {@link module:ui/template~TemplateBinding#observable}. The value may be processed by the
* {@link module:ui/template~TemplateBinding#callback}, if such has been passed to the binding.
*
* @param {Node} [node] A native DOM node, passed to the custom {@link module:ui/template~TemplateBinding#callback}.
* @returns {*} The value of {@link module:ui/template~TemplateBinding#attribute} in
* {@link module:ui/template~TemplateBinding#observable}.
*/
getValue( node ) {
constvalue=this.observable[ this.attribute ];
returnthis.callback ? this.callback( value, node ) : value;
}
/**
* Activates the listener which waits for changes of the {@link module:ui/template~TemplateBinding#attribute} in
* {@link module:ui/template~TemplateBinding#observable}, then updates the DOM with the aggregated
* value of {@link module:ui/template~TemplateValueSchema}.
*
* @param {module:ui/template~TemplateValueSchema} schema A full schema to generate an attribute or text in the DOM.
* @param {Function} updater A DOM updater function used to update the native DOM attribute or text.
* @param {module:ui/template~RenderData} data Rendering data.
* @returns {Function} A function to sever the listener binding.
*/
activateAttributeListener( schema, updater, data ) {
constcallback= () => syncValueSchemaValue( schema, updater, data );
this.emitter.listenTo( this.observable, 'change:' + this.attribute, callback );
// Allows revert of the listener.return () => {
this.emitter.stopListening( this.observable, 'change:' + this.attribute, callback );
};
}
}
exportclassTemplateToBindingextendsTemplateBinding {
/**
* Activates the listener for the native DOM event, which when fired, is propagated by
* the {@link module:ui/template~TemplateBinding#emitter}.
*
* @param {String} domEvtName The name of the native DOM event.
* @param {String} domSelector The selector in the DOM to filter delegated events.
* @param {module:ui/template~RenderData} data Rendering data.
* @returns {Function} A function to sever the listener binding.
*/activateDomEventListener( domEvtName, domSelector, data ) {
constcallback = ( evt, domEvt ) => {
if ( !domSelector || domEvt.target.matches( domSelector ) ) {
if ( typeofthis.eventNameOrFunction == 'function' ) {
this.eventNameOrFunction( domEvt );
} else {
this.observable.fire( this.eventNameOrFunction, domEvt );
}
}
};
this.emitter.listenTo( data.node, domEvtName, callback );
// Allows revert of the listener.return() => {
this.emitter.stopListening( data.node, domEvtName, callback );
};
}
}
exportclassTemplateIfBindingextendsTemplateBinding {
/**
* @inheritDoc
*/getValue( node ) {
const value = super.getValue( node );
returnisFalsy( value ) ? false : ( this.valueIfTrue || true );
}
/**
* The value of the DOM attribute or text to be set if the {@link module:ui/template~TemplateBinding#attribute} in
* {@link module:ui/template~TemplateBinding#observable} is `true`.
*
* @member {String} [module:ui/template~TemplateIfBinding#valueIfTrue]
*/
}
exportdefaultclassTemplate {
/**
* Creates an instance of the {@link ~Template} class.
*
* @param {module:ui/template~TemplateDefinition} def The definition of the template.
*/constructor( def ) {
Object.assign( this, normalize( clone( def ) ) );
/**
* Indicates whether this particular Template instance has been
* {@link #render rendered}.
*
* @readonly
* @protected
* @member {Boolean}
*/this._isRendered = false;
/**
* The tag (`tagName`) of this template, e.g. `div`. It also indicates that the template
* renders to an HTML element.
*
* @member {String} #tag
*//**
* The text of the template. It also indicates that the template renders to a DOM text node.
*
* @member {Array.<String|module:ui/template~TemplateValueSchema>} #text
*//**
* The attributes of the template, e.g. `{ id: [ 'ck-id' ] }`, corresponding with
* the attributes of an HTML element.
*
* **Note**: This property only makes sense when {@link #tag} is defined.
*
* @member {Object} #attributes
*//**
* The children of the template. They can be either:
* * independent instances of {@link ~Template} (sub–templates),
* * native DOM Nodes.
*
* **Note**: This property only makes sense when {@link #tag} is defined.
*
* @member {Array.<module:ui/template~Template|Node>} #children
*//**
* The DOM event listeners of the template.
*
* @member {Object} #eventListeners
*//**
* The data used by the {@link #revert} method to restore a node to its original state.
*
* See: {@link #apply}.
*
* @readonly
* @protected
* @member {module:ui/template~RenderData}
*/this._revertData = null;
}
}
我们首先看构造函数,这个函数实际上就是将一些属性复制到当前模板对象,因此,我们看看具体的方法:
我们先看看clone方法
functionclone( def ) {
const clone = cloneDeepWith( def, value => {
// Don't clone the `Template.bind`* bindings because of the references to Observable// and DomEmitterMixin instances inside, which would also be traversed and cloned by greedy// cloneDeepWith algorithm. There's no point in cloning Observable/DomEmitterMixins// along with the definition.//// Don't clone Template instances if provided as a child. They're simply #render()ed// and nothing should interfere.//// Also don't clone View instances if provided as a child of the Template. The template// instance will be extracted from the View during the normalization and there's no need// to clone it.if ( value && ( value instanceofTemplateBinding || isTemplate( value ) || isView( value ) || isViewCollection( value ) ) ) {
return value;
}
} );
return clone;
}
exportdefaultclassViewCollectionextendsCollection {
constructor( initialItems = [] ) {
super( initialItems, {
// An #id Number attribute should be legal and not break the `ViewCollection` instance.// https://github.com/ckeditor/ckeditor5-ui/issues/93idProperty: 'viewUid'
} );
// Handle {@link module:ui/view~View#element} in DOM when a new view is added to the collection.this.on( 'add', ( evt, view, index ) => {
this._renderViewIntoCollectionParent( view, index );
} );
// Handle {@link module:ui/view~View#element} in DOM when a view is removed from the collection.this.on( 'remove', ( evt, view ) => {
if ( view.element && this._parentElement ) {
view.element.remove();
}
} );
/**
* A parent element within which child views are rendered and managed in DOM.
*
* @protected
* @member {HTMLElement}
*/this._parentElement = null;
}
}
setParent( elementOrDocFragment ) {
this._parentElement = elementOrDocFragment;
// Take care of the initial collection items passed to the constructor.for ( const view ofthis ) {
this._renderViewIntoCollectionParent( view );
}
}
classProxyEmitter {
/**
* @param {Node} node DOM Node that fires events.
* @param {Object} [options] Additional options.
* @param {Boolean} [options.useCapture=false] Indicates that events of this type will be dispatched to the registered
* listener before being dispatched to any EventTarget beneath it in the DOM tree.
* @param {Boolean} [options.usePassive=false] Indicates that the function specified by listener will never call preventDefault()
* and prevents blocking browser's main thread by this event handler.
*/constructor( node, options ) {
// Set emitter ID to match DOM Node "expando" property._setEmitterId( this, getProxyEmitterId( node, options ) );
// Remember the DOM Node this ProxyEmitter is bound to.this._domNode = node;
// And given options.this._options = options;
}
}
extend( ProxyEmitter.prototype, EmitterMixin, {
/**
* Collection of native DOM listeners.
*
* @private
* @member {Object} module:utils/dom/emittermixin~ProxyEmitter#_domListeners
*//**
* Registers a callback function to be executed when an event is fired.
*
* It attaches a native DOM listener to the DOM Node. When fired,
* a corresponding Emitter event will also fire with DOM Event object as an argument.
*
* **Note**: This is automatically called by the
* {@link module:utils/emittermixin~EmitterMixin#listenTo `EmitterMixin#listenTo()`}.
*
* @method module:utils/dom/emittermixin~ProxyEmitter#attach
* @param {String} event The name of the event.
*/attach( event ) {
// If the DOM Listener for given event already exist it is pointless// to attach another one.if ( this._domListeners && this._domListeners[ event ] ) {
return;
}
const domListener = this._createDomListener( event );
// Attach the native DOM listener to DOM Node.this._domNode.addEventListener( event, domListener, this._options );
if ( !this._domListeners ) {
this._domListeners = {};
}
// Store the native DOM listener in this ProxyEmitter. It will be helpful// when stopping listening to the event.this._domListeners[ event ] = domListener;
},
/**
* Stops executing the callback on the given event.
*
* **Note**: This is automatically called by the
* {@link module:utils/emittermixin~EmitterMixin#stopListening `EmitterMixin#stopListening()`}.
*
* @method module:utils/dom/emittermixin~ProxyEmitter#detach
* @param {String} event The name of the event.
*/detach( event ) {
let events;
// Remove native DOM listeners which are orphans. If no callbacks// are awaiting given event, detach native DOM listener from DOM Node.// See: {@link attach}.if ( this._domListeners[ event ] && ( !( events = this._events[ event ] ) || !events.callbacks.length ) ) {
this._domListeners[ event ].removeListener();
}
},
/**
* Adds callback to emitter for given event.
*
* @protected
* @method module:utils/dom/emittermixin~ProxyEmitter#_addEventListener
* @param {String} event The name of the event.
* @param {Function} callback The function to be called on event.
* @param {Object} [options={}] Additional options.
* @param {module:utils/priorities~PriorityString|Number} [options.priority='normal'] The priority of this event callback. The higher
* the priority value the sooner the callback will be fired. Events having the same priority are called in the
* order they were added.
*/_addEventListener( event, callback, options ) {
this.attach( event );
EmitterMixin._addEventListener.call( this, event, callback, options );
},
/**
* Removes callback from emitter for given event.
*
* @protected
* @method module:utils/dom/emittermixin~ProxyEmitter#_removeEventListener
* @param {String} event The name of the event.
* @param {Function} callback The function to stop being called.
*/_removeEventListener( event, callback ) {
EmitterMixin._removeEventListener.call( this, event, callback );
this.detach( event );
},
/**
* Creates a native DOM listener callback. When the native DOM event
* is fired it will fire corresponding event on this ProxyEmitter.
* Note: A native DOM Event is passed as an argument.
*
* @private
* @method module:utils/dom/emittermixin~ProxyEmitter#_createDomListener
* @param {String} event The name of the event.
* @returns {Function} The DOM listener callback.
*/_createDomListener( event ) {
constdomListener = domEvt => {
this.fire( event, domEvt );
};
// Supply the DOM listener callback with a function that will help// detach it from the DOM Node, when it is no longer necessary.// See: {@link detach}.
domListener.removeListener = () => {
this._domNode.removeEventListener( event, domListener, this._options );
deletethis._domListeners[ event ];
};
return domListener;
}
} );
exportdefaultclassCollection {
constructor( initialItemsOrOptions = {}, options = {} ) {
const hasInitialItems = isIterable( initialItemsOrOptions );
if ( !hasInitialItems ) {
options = initialItemsOrOptions;
}
/**
* The internal list of items in the collection.
*
* @private
* @member {Object[]}
*/this._items = [];
/**
* The internal map of items in the collection.
*
* @private
* @member {Map}
*/this._itemMap = newMap();
/**
* The name of the property which is considered to identify an item.
*
* @private
* @member {String}
*/this._idProperty = options.idProperty || 'id';
/**
* A helper mapping external items of a bound collection ({@link #bindTo})
* and actual items of this collection. It provides information
* necessary to properly remove items bound to another collection.
*
* See {@link #_bindToInternalToExternalMap}.
*
* @protected
* @member {WeakMap}
*/this._bindToExternalToInternalMap = newWeakMap();
/**
* A helper mapping items of this collection to external items of a bound collection
* ({@link #bindTo}). It provides information necessary to manage the bindings, e.g.
* to avoid loops in two–way bindings.
*
* See {@link #_bindToExternalToInternalMap}.
*
* @protected
* @member {WeakMap}
*/this._bindToInternalToExternalMap = newWeakMap();
/**
* Stores indexes of skipped items from bound external collection.
*
* @private
* @member {Array}
*/this._skippedIndexesFromExternal = [];
// Set the initial content of the collection (if provided in the constructor).if ( hasInitialItems ) {
for ( const item of initialItemsOrOptions ) {
this._items.push( item );
this._itemMap.set( this._getItemIdBeforeAdding( item ), item );
}
}
/**
* A collection instance this collection is bound to as a result
* of calling {@link #bindTo} method.
*
* @protected
* @member {module:utils/collection~Collection} #_bindToCollection
*/
}
}
/**
* Finalizes and activates a binding initiated by {#bindTo}.
*
* @protected
* @param {Function} factory A function which produces collection items.
*/_setUpBindToBinding( factory ) {
const externalCollection = this._bindToCollection;
// Adds the item to the collection once a change has been done to the external collection.//// @privateconstaddItem = ( evt, externalItem, index ) => {
const isExternalBoundToThis = externalCollection._bindToCollection == this;
const externalItemBound = externalCollection._bindToInternalToExternalMap.get( externalItem );
// If an external collection is bound to this collection, which makes it a 2–way binding,// and the particular external collection item is already bound, don't add it here.// The external item has been created **out of this collection's item** and (re)adding it will// cause a loop.if ( isExternalBoundToThis && externalItemBound ) {
this._bindToExternalToInternalMap.set( externalItem, externalItemBound );
this._bindToInternalToExternalMap.set( externalItemBound, externalItem );
} else {
const item = factory( externalItem );
// When there is no item we need to remember skipped index first and then we can skip this item.if ( !item ) {
this._skippedIndexesFromExternal.push( index );
return;
}
// Lets try to put item at the same index as index in external collection// but when there are a skipped items in one or both collections we need to recalculate this index.let finalIndex = index;
// When we try to insert item after some skipped items from external collection we need// to include this skipped items and decrease index.//// For the following example:// external -> [ 'A', 'B - skipped for internal', 'C - skipped for internal' ]// internal -> [ A ]//// Another item is been added at the end of external collection:// external.add( 'D' )// external -> [ 'A', 'B - skipped for internal', 'C - skipped for internal', 'D' ]//// We can't just add 'D' to internal at the same index as index in external because// this will produce empty indexes what is invalid:// internal -> [ 'A', empty, empty, 'D' ]//// So we need to include skipped items and decrease index// internal -> [ 'A', 'D' ]for ( const skipped ofthis._skippedIndexesFromExternal ) {
if ( index > skipped ) {
finalIndex--;
}
}
// We need to take into consideration that external collection could skip some items from// internal collection.//// For the following example:// internal -> [ 'A', 'B - skipped for external', 'C - skipped for external' ]// external -> [ A ]//// Another item is been added at the end of external collection:// external.add( 'D' )// external -> [ 'A', 'D' ]//// We need to include skipped items and place new item after them:// internal -> [ 'A', 'B - skipped for external', 'C - skipped for external', 'D' ]for ( const skipped of externalCollection._skippedIndexesFromExternal ) {
if ( finalIndex >= skipped ) {
finalIndex++;
}
}
this._bindToExternalToInternalMap.set( externalItem, item );
this._bindToInternalToExternalMap.set( item, externalItem );
this.add( item, finalIndex );
// After adding new element to internal collection we need update indexes// of skipped items in external collection.for ( let i = 0; i < externalCollection._skippedIndexesFromExternal.length; i++ ) {
if ( finalIndex <= externalCollection._skippedIndexesFromExternal[ i ] ) {
externalCollection._skippedIndexesFromExternal[ i ]++;
}
}
}
};
// Load the initial content of the collection.for ( const externalItem of externalCollection ) {
addItem( null, externalItem, externalCollection.getIndex( externalItem ) );
}
// Synchronize the with collection as new items are added.this.listenTo( externalCollection, 'add', addItem );
// Synchronize the with collection as new items are removed.this.listenTo( externalCollection, 'remove', ( evt, externalItem, index ) => {
const item = this._bindToExternalToInternalMap.get( externalItem );
if ( item ) {
this.remove( item );
}
// After removing element from external collection we need update/remove indexes// of skipped items in internal collection.this._skippedIndexesFromExternal = this._skippedIndexesFromExternal.reduce( ( result, skipped ) => {
if ( index < skipped ) {
result.push( skipped - 1 );
}
if ( index > skipped ) {
result.push( skipped );
}
return result;
}, [] );
} );
}
add( item, index ) {
returnthis.addMany( [ item ], index );
}
/**
* Adds multiple items into the collection.
*
* Any item not containing an id will get an automatically generated one.
*
* @chainable
* @param {Iterable.<Object>} item
* @param {Number} [index] The position of the insertion. Items will be appended if no `index` is specified.
* @fires add
* @fires change
*/addMany( items, index ) {
if ( index === undefined ) {
index = this._items.length;
} elseif ( index > this._items.length || index < 0 ) {
/**
* The `index` passed to {@link module:utils/collection~Collection#addMany `Collection#addMany()`}
* is invalid. It must be a number between 0 and the collection's length.
*
* @error collection-add-item-invalid-index
*/thrownewCKEditorError( 'collection-add-item-invalid-index', this );
}
for ( let offset = 0; offset < items.length; offset++ ) {
const item = items[ offset ];
const itemId = this._getItemIdBeforeAdding( item );
const currentItemIndex = index + offset;
this._items.splice( currentItemIndex, 0, item );
this._itemMap.set( itemId, item );
this.fire( 'add', item, currentItemIndex );
}
this.fire( 'change', {
added: items,
removed: [],
index
} );
returnthis;
}
remove( subject ) {
const [ item, index ] = this._remove( subject );
this.fire( 'change', {
added: [],
removed: [ item ],
index
} );
return item;
}
createEventNamespace( this, event );
const lists = getCallbacksListsForNamespace( this, event );
const priority = priorities.get( options.priority );
const callbackDefinition = {
callback,
priority
};
// Add the callback to all callbacks list.for ( const callbacks of lists ) {
// Add the callback to the list in the right priority position.insertToPriorityArray( callbacks, callbackDefinition );
}
fire( eventOrInfo, ...args ) {
try {
const eventInfo = eventOrInfo instanceofEventInfo ? eventOrInfo : newEventInfo( this, eventOrInfo );
const event = eventInfo.name;
let callbacks = getCallbacksForEvent( this, event );
// Record that the event passed this emitter on its path.
eventInfo.path.push( this );
// Handle event listener callbacks first.if ( callbacks ) {
// Arguments passed to each callback.const callbackArgs = [ eventInfo, ...args ];
// Copying callbacks array is the easiest and most secure way of preventing infinite loops, when event callbacks// are added while processing other callbacks. Previous solution involved adding counters (unique ids) but// failed if callbacks were added to the queue before currently processed callback.// If this proves to be too inefficient, another method is to change `.on()` so callbacks are stored if same// event is currently processed. Then, `.fire()` at the end, would have to add all stored events.
callbacks = Array.from( callbacks );
for ( let i = 0; i < callbacks.length; i++ ) {
callbacks[ i ].callback.apply( this, callbackArgs );
// Remove the callback from future requests if off() has been called.if ( eventInfo.off.called ) {
// Remove the called mark for the next calls.delete eventInfo.off.called;
this._removeEventListener( event, callbacks[ i ].callback );
}
// Do not execute next callbacks if stop() was called.if ( eventInfo.stop.called ) {
break;
}
}
}
// Delegate event to other emitters if needed.if ( this._delegations ) {
const destinations = this._delegations.get( event );
const passAllDestinations = this._delegations.get( '*' );
if ( destinations ) {
fireDelegatedEvents( destinations, eventInfo, args );
}
if ( passAllDestinations ) {
fireDelegatedEvents( passAllDestinations, eventInfo, args );
}
}
return eventInfo.return;
} catch ( err ) {
// @if CK_DEBUG // throw err;/* istanbul ignore next */CKEditorError.rethrowUnexpectedError( err, this );
}
},
重点方法
这里面有一个重点方法:
// Get the list of callbacks for a given event, but only if there any callbacks have been registered.// If there are no callbacks registered for given event, it checks if this is a specific event and looks// for callbacks for it's more generic version.functiongetCallbacksForEvent( source, eventName ) {
let event;
if ( !source._events || !( event = source._events[ eventName ] ) || !event.callbacks.length ) {
// There are no callbacks registered for specified eventName.// But this could be a specific-type event that is in a namespace.if ( eventName.indexOf( ':' ) > -1 ) {
// If the eventName is specific, try to find callback lists for more generic event.returngetCallbacksForEvent( source, eventName.substr( 0, eventName.lastIndexOf( ':' ) ) );
} else {
// If this is a top-level generic event, return null;returnnull;
}
}
return event.callbacks;
}
const anyClass = newAnyClass();
//注意,这里假定AnotherClass也是一个Emitterconst anotherClass = newAnotherClass();
// Stop listening to a specific handler.
anyClass.stopListening( anotherClass, 'eventName', handler );
// Stop listening to a specific event.
anyClass.stopListening( anotherClass, 'eventName' );
// Stop listening to all events fired by a specific emitter.
anyClass.stopListening( anotherClass);
// Stop listening to all events fired by all bound emitters.
anyClass.stopListening();
// Rect of an HTMLElement. 这里是常用dom元素const rectA = newRect( document.body );
// Rect of a DOM Range. 这里是选择区域的第一个Rangeconst rectB = newRect( document.getSelection().getRangeAt( 0 ) );
// Rect of a window (web browser viewport). window对象const rectC = newRect( window );
// Rect out of an object. 实际上就是Object对象const rectD = newRect( { top: 0, right: 10, bottom: 10, left: 0, width: 10, height: 10 } );
// Rect out of another Rect instance. const rectE = newRect( rectD );
// Rect out of a ClientRect. //这里是一个ClientRect对象const rectF = newRect( document.body.getClientRects().item( 0 ) );
excludeScrollbarsAndBorders() → Rect
调用这个方法就可以返回一个不包含边框和滚动条的Rect对象。
另外Rect这个类还包含一些比较有用的方法,简单说一下:
clone() : Rect
复制一个对象
contains(anotherRect) : Boolean
当前对象是否包含参数对象
getArea() : Number
返回Rect的面积
getIntersection(anotherRect) : Rect
返回两个Rect的交集
getIntersectionArea(anotherRect) : Number
返回两个Rect的交集的面积
getVisible() : Rect | null
返回一个新的矩形,原始矩形的一部分,它实际上对用户可见,例如由父元素 rect 裁剪的原始 rect,其在 CSS 中设置了溢出而不是“可见”。如果没有这样的可见矩形,即当矩形被一个或多个祖先限制时,则返回 null。
target : HTMLElement | Range | Window | ClientRect | DOMRect | Rect | Object | function
这个是定位元素相对的目标元素
positions : Array<positioningFunction>>
一组定位使用的函数
这三个元素值最重要的属性,另外三个属性可以查看官网的说明
这里再介绍一下定位函数
positioningFunction
定位函数有三个重要的参数
第一个是targetRect,第二个是elementRect,第三个是viewportRect
// This simple position will place the element directly under the target, in the middle://// [ Target ]// +-----------------+// | Element |// +-----------------+//constposition = ( targetRect, elementRect, [ viewportRect ] ) => ( {
top: targetRect.bottom,
left: targetRect.left + targetRect.width / 2 - elementRect.width / 2,
name: 'bottomMiddle',
// Note: The config is optional.config: {
zIndex: '999'
}
} );
functiongetEvents( source ) {
if ( !source._events ) {
Object.defineProperty( source, '_events', {
value: {}
} );
}
return source._events;
}
functionmakeEventNode() {
return {
callbacks: [],
childEvents: []
};
}
functioncreateEventNamespace( source, eventName ) {
const events = getEvents( source );
// First, check if the event we want to add to the structure already exists.if ( events[ eventName ] ) {
// If it exists, we don't have to do anything.return;
}
// In other case, we have to create the structure for the event.// Note, that we might need to create intermediate events too.// I.e. if foo:bar:abc is being registered and we only have foo in the structure,// we need to also register foo:bar.// Currently processed event name.let name = eventName;
// Name of the event that is a child event for currently processed event.let childEventName = null;
// Array containing all newly created specific events.const newEventNodes = [];
// While loop can't check for ':' index because we have to handle generic events too.// In each loop, we truncate event name, going from the most specific name to the generic one.// I.e. foo:bar:abc -> foo:bar -> foo.while ( name !== '' ) {
if ( events[ name ] ) {
// If the currently processed event name is already registered, we can be sure// that it already has all the structure created, so we can break the loop here// as no more events need to be registered.break;
}
// If this event is not yet registered, create a new object for it.
events[ name ] = makeEventNode();
// Add it to the array with newly created events.
newEventNodes.push( events[ name ] );
// Add previously processed event name as a child of this event.if ( childEventName ) {
events[ name ].childEvents.push( childEventName );
}
childEventName = name;
// If `.lastIndexOf()` returns -1, `.substr()` will return '' which will break the loop.
name = name.substr( 0, name.lastIndexOf( ':' ) );
}
if ( name !== '' ) {
// If name is not empty, we found an already registered event that was a parent of the// event we wanted to register.// Copy that event's callbacks to newly registered events.for ( const node of newEventNodes ) {
node.callbacks = events[ name ].callbacks.slice();
}
// Add last newly created event to the already registered event.
events[ name ].childEvents.push( childEventName );
}
return events;
}
module.exports = {
createEventNamespace
}
import {View} from 'ckeditor5/src/ui';
export default class Button extends View {
constructor(){
super();
this.type = 'button';
const bind = this.bindTemplate;
// this.label is observable but undefined.
this.set( 'label' );
// this.isOn is observable and false.
this.set( 'isOn', false );
// this.isEnabled is observable and true.
this.set( 'isEnabled', true );
this.setTemplate( {
tag: 'button',
attributes: {
class: [
// The 'ck-on' and 'ck-off' classes toggle according to the #isOn property.
bind.to( 'isOn', value => value ? 'ck-on' : 'ck-off' ),
// The 'ck-enabled' class appears when the #isEnabled property is false.
bind.if( 'isEnabled', 'ck-disabled', value => !value )
],
type: this.type
},
children: [
{
// The text of the button is bound to the #label property.
text: bind.to( 'label' )
}
]
} );
}
}
emitterA.delegate( 'foo' ).to( emitterB, 'bar' );
emitterA.delegate( 'foo' ).to( emitterB, name =>`delegated:${ name }` );
3、代理所有事件
emitterA.delegate( '*' ).to( emitterB );
4、取消代理事件
// Stop delegating all events.
emitterA.stopDelegating();
// Stop delegating a specific event to all emitters.
emitterA.stopDelegating( 'foo' );
// Stop delegating a specific event to a specific emitter.
emitterA.stopDelegating( 'foo', emitterB );