new ckeditor
New ckeditor
This commit is contained in:
53
ckeditor/core/dom/comment.js
Normal file
53
ckeditor/core/dom/comment.js
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
|
||||
* For licensing, see LICENSE.md or http://ckeditor.com/license
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileOverview Defines the {@link CKEDITOR.dom.comment} class, which represents
|
||||
* a DOM comment node.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents a DOM comment node.
|
||||
*
|
||||
* var nativeNode = document.createComment( 'Example' );
|
||||
* var comment = new CKEDITOR.dom.comment( nativeNode );
|
||||
*
|
||||
* var comment = new CKEDITOR.dom.comment( 'Example' );
|
||||
*
|
||||
* @class
|
||||
* @extends CKEDITOR.dom.node
|
||||
* @constructor Creates a comment class instance.
|
||||
* @param {Object/String} comment A native DOM comment node or a string containing
|
||||
* the text to use to create a new comment node.
|
||||
* @param {CKEDITOR.dom.document} [ownerDocument] The document that will contain
|
||||
* the node in case of new node creation. Defaults to the current document.
|
||||
*/
|
||||
CKEDITOR.dom.comment = function( comment, ownerDocument ) {
|
||||
if ( typeof comment == 'string' )
|
||||
comment = ( ownerDocument ? ownerDocument.$ : document ).createComment( comment );
|
||||
|
||||
CKEDITOR.dom.domObject.call( this, comment );
|
||||
};
|
||||
|
||||
CKEDITOR.dom.comment.prototype = new CKEDITOR.dom.node();
|
||||
|
||||
CKEDITOR.tools.extend( CKEDITOR.dom.comment.prototype, {
|
||||
/**
|
||||
* The node type. This is a constant value set to {@link CKEDITOR#NODE_COMMENT}.
|
||||
*
|
||||
* @readonly
|
||||
* @property {Number} [=CKEDITOR.NODE_COMMENT]
|
||||
*/
|
||||
type: CKEDITOR.NODE_COMMENT,
|
||||
|
||||
/**
|
||||
* Gets the outer HTML of this comment.
|
||||
*
|
||||
* @returns {String} The HTML `<!-- comment value -->`.
|
||||
*/
|
||||
getOuterHtml: function() {
|
||||
return '<!--' + this.$.nodeValue + '-->';
|
||||
}
|
||||
} );
|
||||
326
ckeditor/core/dom/document.js
Normal file
326
ckeditor/core/dom/document.js
Normal file
@@ -0,0 +1,326 @@
|
||||
/**
|
||||
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
|
||||
* For licensing, see LICENSE.md or http://ckeditor.com/license
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileOverview Defines the {@link CKEDITOR.dom.document} class which
|
||||
* represents a DOM document.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents a DOM document.
|
||||
*
|
||||
* var document = new CKEDITOR.dom.document( document );
|
||||
*
|
||||
* @class
|
||||
* @extends CKEDITOR.dom.domObject
|
||||
* @constructor Creates a document class instance.
|
||||
* @param {Object} domDocument A native DOM document.
|
||||
*/
|
||||
CKEDITOR.dom.document = function( domDocument ) {
|
||||
CKEDITOR.dom.domObject.call( this, domDocument );
|
||||
};
|
||||
|
||||
// PACKAGER_RENAME( CKEDITOR.dom.document )
|
||||
|
||||
CKEDITOR.dom.document.prototype = new CKEDITOR.dom.domObject();
|
||||
|
||||
CKEDITOR.tools.extend( CKEDITOR.dom.document.prototype, {
|
||||
/**
|
||||
* The node type. This is a constant value set to {@link CKEDITOR#NODE_DOCUMENT}.
|
||||
*
|
||||
* @readonly
|
||||
* @property {Number} [=CKEDITOR.NODE_DOCUMENT]
|
||||
*/
|
||||
type: CKEDITOR.NODE_DOCUMENT,
|
||||
|
||||
/**
|
||||
* Appends a CSS file to the document.
|
||||
*
|
||||
* CKEDITOR.document.appendStyleSheet( '/mystyles.css' );
|
||||
*
|
||||
* @param {String} cssFileUrl The CSS file URL.
|
||||
*/
|
||||
appendStyleSheet: function( cssFileUrl ) {
|
||||
if ( this.$.createStyleSheet )
|
||||
this.$.createStyleSheet( cssFileUrl );
|
||||
else {
|
||||
var link = new CKEDITOR.dom.element( 'link' );
|
||||
link.setAttributes( {
|
||||
rel: 'stylesheet',
|
||||
type: 'text/css',
|
||||
href: cssFileUrl
|
||||
} );
|
||||
|
||||
this.getHead().append( link );
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates a CSS stylesheet and inserts it into the document.
|
||||
*
|
||||
* @param cssStyleText {String} CSS style text.
|
||||
* @returns {Object} The created DOM native stylesheet object.
|
||||
*/
|
||||
appendStyleText: function( cssStyleText ) {
|
||||
if ( this.$.createStyleSheet ) {
|
||||
var styleSheet = this.$.createStyleSheet( '' );
|
||||
styleSheet.cssText = cssStyleText;
|
||||
} else {
|
||||
var style = new CKEDITOR.dom.element( 'style', this );
|
||||
style.append( new CKEDITOR.dom.text( cssStyleText, this ) );
|
||||
this.getHead().append( style );
|
||||
}
|
||||
|
||||
return styleSheet || style.$.sheet;
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates a {@link CKEDITOR.dom.element} instance in this document.
|
||||
*
|
||||
* @param {String} name The name of the element.
|
||||
* @param {Object} [attributesAndStyles]
|
||||
* @param {Object} [attributesAndStyles.attributes] Attributes that will be set.
|
||||
* @param {Object} [attributesAndStyles.styles] Styles that will be set.
|
||||
* @returns {CKEDITOR.dom.element}
|
||||
*/
|
||||
createElement: function( name, attribsAndStyles ) {
|
||||
var element = new CKEDITOR.dom.element( name, this );
|
||||
|
||||
if ( attribsAndStyles ) {
|
||||
if ( attribsAndStyles.attributes )
|
||||
element.setAttributes( attribsAndStyles.attributes );
|
||||
|
||||
if ( attribsAndStyles.styles )
|
||||
element.setStyles( attribsAndStyles.styles );
|
||||
}
|
||||
|
||||
return element;
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates a {@link CKEDITOR.dom.text} instance in this document.
|
||||
*
|
||||
* @param {String} text Value of the text node.
|
||||
* @returns {CKEDITOR.dom.element}
|
||||
*/
|
||||
createText: function( text ) {
|
||||
return new CKEDITOR.dom.text( text, this );
|
||||
},
|
||||
|
||||
/**
|
||||
* Moves the selection focus to this document's window.
|
||||
*/
|
||||
focus: function() {
|
||||
this.getWindow().focus();
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the element that is currently designated as the active element in the document.
|
||||
*
|
||||
* **Note:** Only one element can be active at a time in a document.
|
||||
* An active element does not necessarily have focus,
|
||||
* but an element with focus is always the active element in a document.
|
||||
*
|
||||
* @returns {CKEDITOR.dom.element} Active element or `null` if an IE8-9 bug is encountered.
|
||||
* See [#10030](http://dev.ckeditor.com/ticket/10030).
|
||||
*/
|
||||
getActive: function() {
|
||||
var $active;
|
||||
try {
|
||||
$active = this.$.activeElement;
|
||||
} catch ( e ) {
|
||||
return null;
|
||||
}
|
||||
return new CKEDITOR.dom.element( $active );
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets an element based on its ID.
|
||||
*
|
||||
* var element = CKEDITOR.document.getById( 'myElement' );
|
||||
* alert( element.getId() ); // 'myElement'
|
||||
*
|
||||
* @param {String} elementId The element ID.
|
||||
* @returns {CKEDITOR.dom.element} The element instance, or `null` if not found.
|
||||
*/
|
||||
getById: function( elementId ) {
|
||||
var $ = this.$.getElementById( elementId );
|
||||
return $ ? new CKEDITOR.dom.element( $ ) : null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets a node based on its address. See {@link CKEDITOR.dom.node#getAddress}.
|
||||
*
|
||||
* @param {Array} address
|
||||
* @param {Boolean} [normalized=false]
|
||||
*/
|
||||
getByAddress: function( address, normalized ) {
|
||||
var $ = this.$.documentElement;
|
||||
|
||||
for ( var i = 0; $ && i < address.length; i++ ) {
|
||||
var target = address[ i ];
|
||||
|
||||
if ( !normalized ) {
|
||||
$ = $.childNodes[ target ];
|
||||
continue;
|
||||
}
|
||||
|
||||
var currentIndex = -1;
|
||||
|
||||
for ( var j = 0; j < $.childNodes.length; j++ ) {
|
||||
var candidate = $.childNodes[ j ];
|
||||
|
||||
if ( normalized === true && candidate.nodeType == 3 && candidate.previousSibling && candidate.previousSibling.nodeType == 3 )
|
||||
continue;
|
||||
|
||||
currentIndex++;
|
||||
|
||||
if ( currentIndex == target ) {
|
||||
$ = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $ ? new CKEDITOR.dom.node( $ ) : null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets elements list based on a given tag name.
|
||||
*
|
||||
* @param {String} tagName The element tag name.
|
||||
* @returns {CKEDITOR.dom.nodeList} The nodes list.
|
||||
*/
|
||||
getElementsByTag: function( tagName, namespace ) {
|
||||
if ( !( CKEDITOR.env.ie && ( document.documentMode <= 8 ) ) && namespace )
|
||||
tagName = namespace + ':' + tagName;
|
||||
return new CKEDITOR.dom.nodeList( this.$.getElementsByTagName( tagName ) );
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the `<head>` element for this document.
|
||||
*
|
||||
* var element = CKEDITOR.document.getHead();
|
||||
* alert( element.getName() ); // 'head'
|
||||
*
|
||||
* @returns {CKEDITOR.dom.element} The `<head>` element.
|
||||
*/
|
||||
getHead: function() {
|
||||
var head = this.$.getElementsByTagName( 'head' )[ 0 ];
|
||||
if ( !head )
|
||||
head = this.getDocumentElement().append( new CKEDITOR.dom.element( 'head' ), true );
|
||||
else
|
||||
head = new CKEDITOR.dom.element( head );
|
||||
|
||||
return head;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the `<body>` element for this document.
|
||||
*
|
||||
* var element = CKEDITOR.document.getBody();
|
||||
* alert( element.getName() ); // 'body'
|
||||
*
|
||||
* @returns {CKEDITOR.dom.element} The `<body>` element.
|
||||
*/
|
||||
getBody: function() {
|
||||
return new CKEDITOR.dom.element( this.$.body );
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the DOM document element for this document.
|
||||
*
|
||||
* @returns {CKEDITOR.dom.element} The DOM document element.
|
||||
*/
|
||||
getDocumentElement: function() {
|
||||
return new CKEDITOR.dom.element( this.$.documentElement );
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the window object that stores this document.
|
||||
*
|
||||
* @returns {CKEDITOR.dom.window} The window object.
|
||||
*/
|
||||
getWindow: function() {
|
||||
return new CKEDITOR.dom.window( this.$.parentWindow || this.$.defaultView );
|
||||
},
|
||||
|
||||
/**
|
||||
* Defines the document content through `document.write`. Note that the
|
||||
* previous document content will be lost (cleaned).
|
||||
*
|
||||
* document.write(
|
||||
* '<html>' +
|
||||
* '<head><title>Sample Document</title></head>' +
|
||||
* '<body>Document content created by code.</body>' +
|
||||
* '</html>'
|
||||
* );
|
||||
*
|
||||
* @since 3.5
|
||||
* @param {String} html The HTML defining the document content.
|
||||
*/
|
||||
write: function( html ) {
|
||||
// Don't leave any history log in IE. (#5657)
|
||||
this.$.open( 'text/html', 'replace' );
|
||||
|
||||
// Support for custom document.domain in IE.
|
||||
//
|
||||
// The script must be appended because if placed before the
|
||||
// doctype, IE will go into quirks mode and mess with
|
||||
// the editable, e.g. by changing its default height.
|
||||
if ( CKEDITOR.env.ie )
|
||||
html = html.replace( /(?:^\s*<!DOCTYPE[^>]*?>)|^/i, '$&\n<script data-cke-temp="1">(' + CKEDITOR.tools.fixDomain + ')();</script>' );
|
||||
|
||||
this.$.write( html );
|
||||
this.$.close();
|
||||
},
|
||||
|
||||
/**
|
||||
* Wrapper for `querySelectorAll`. Returns a list of elements within this document that match
|
||||
* the specified `selector`.
|
||||
*
|
||||
* **Note:** The returned list is not a live collection (like the result of native `querySelectorAll`).
|
||||
*
|
||||
* @since 4.3
|
||||
* @param {String} selector
|
||||
* @returns {CKEDITOR.dom.nodeList}
|
||||
*/
|
||||
find: function( selector ) {
|
||||
return new CKEDITOR.dom.nodeList( this.$.querySelectorAll( selector ) );
|
||||
},
|
||||
|
||||
/**
|
||||
* Wrapper for `querySelector`. Returns the first element within this document that matches
|
||||
* the specified `selector`.
|
||||
*
|
||||
* @since 4.3
|
||||
* @param {String} selector
|
||||
* @returns {CKEDITOR.dom.element}
|
||||
*/
|
||||
findOne: function( selector ) {
|
||||
var el = this.$.querySelector( selector );
|
||||
|
||||
return el ? new CKEDITOR.dom.element( el ) : null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Internet Explorer 8 only method. It returns a document fragment which has all HTML5 elements enabled.
|
||||
*
|
||||
* @since 4.3
|
||||
* @private
|
||||
* @returns DocumentFragment
|
||||
*/
|
||||
_getHtml5ShivFrag: function() {
|
||||
var $frag = this.getCustomData( 'html5ShivFrag' );
|
||||
|
||||
if ( !$frag ) {
|
||||
$frag = this.$.createDocumentFragment();
|
||||
CKEDITOR.tools.enableHtml5Elements( $frag, true );
|
||||
this.setCustomData( 'html5ShivFrag', $frag );
|
||||
}
|
||||
|
||||
return $frag;
|
||||
}
|
||||
} );
|
||||
62
ckeditor/core/dom/documentfragment.js
Normal file
62
ckeditor/core/dom/documentfragment.js
Normal file
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
|
||||
* For licensing, see LICENSE.md or http://ckeditor.com/license
|
||||
*/
|
||||
|
||||
/**
|
||||
* DocumentFragment is a "lightweight" or "minimal" Document object. It is
|
||||
* commonly used to extract a portion of a document's tree or to create a new
|
||||
* fragment of a document. Various operations may take document fragment objects
|
||||
* as arguments and result in all the child nodes of the document fragment being
|
||||
* moved to the child list of this node.
|
||||
*
|
||||
* @class
|
||||
* @constructor Creates a document fragment class instance.
|
||||
* @param {CKEDITOR.dom.document/DocumentFragment} [nodeOrDoc=CKEDITOR.document]
|
||||
*/
|
||||
CKEDITOR.dom.documentFragment = function( nodeOrDoc ) {
|
||||
nodeOrDoc = nodeOrDoc || CKEDITOR.document;
|
||||
|
||||
if ( nodeOrDoc.type == CKEDITOR.NODE_DOCUMENT )
|
||||
this.$ = nodeOrDoc.$.createDocumentFragment();
|
||||
else
|
||||
this.$ = nodeOrDoc;
|
||||
};
|
||||
|
||||
CKEDITOR.tools.extend( CKEDITOR.dom.documentFragment.prototype, CKEDITOR.dom.element.prototype, {
|
||||
/**
|
||||
* The node type. This is a constant value set to {@link CKEDITOR#NODE_DOCUMENT_FRAGMENT}.
|
||||
*
|
||||
* @readonly
|
||||
* @property {Number} [=CKEDITOR.NODE_DOCUMENT_FRAGMENT]
|
||||
*/
|
||||
type: CKEDITOR.NODE_DOCUMENT_FRAGMENT,
|
||||
|
||||
/**
|
||||
* Inserts the document fragment content after the specified node.
|
||||
*
|
||||
* @param {CKEDITOR.dom.node} node
|
||||
*/
|
||||
insertAfterNode: function( node ) {
|
||||
node = node.$;
|
||||
node.parentNode.insertBefore( this.$, node.nextSibling );
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets HTML of this document fragment's children.
|
||||
*
|
||||
* @since 4.5
|
||||
* @returns {String} The HTML of this document fragment's children.
|
||||
*/
|
||||
getHtml: function() {
|
||||
var container = new CKEDITOR.dom.element( 'div' );
|
||||
|
||||
this.clone( 1, 1 ).appendTo( container );
|
||||
|
||||
return container.getHtml().replace( /\s*data-cke-expando=".*?"/g, '' );
|
||||
}
|
||||
}, true, {
|
||||
'append': 1, 'appendBogus': 1, 'clone': 1, 'getFirst': 1, 'getHtml': 1, 'getLast': 1, 'getParent': 1, 'getNext': 1, 'getPrevious': 1,
|
||||
'appendTo': 1, 'moveChildren': 1, 'insertBefore': 1, 'insertAfterNode': 1, 'replace': 1, 'trim': 1, 'type': 1,
|
||||
'ltrim': 1, 'rtrim': 1, 'getDocument': 1, 'getChildCount': 1, 'getChild': 1, 'getChildren': 1
|
||||
} );
|
||||
266
ckeditor/core/dom/domobject.js
Normal file
266
ckeditor/core/dom/domobject.js
Normal file
@@ -0,0 +1,266 @@
|
||||
/**
|
||||
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
|
||||
* For licensing, see LICENSE.md or http://ckeditor.com/license
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileOverview Defines the {@link CKEDITOR.editor} class, which is the base
|
||||
* for other classes representing DOM objects.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents a DOM object. This class is not intended to be used directly. It
|
||||
* serves as the base class for other classes representing specific DOM
|
||||
* objects.
|
||||
*
|
||||
* @class
|
||||
* @mixins CKEDITOR.event
|
||||
* @constructor Creates a domObject class instance.
|
||||
* @param {Object} nativeDomObject A native DOM object.
|
||||
*/
|
||||
CKEDITOR.dom.domObject = function( nativeDomObject ) {
|
||||
if ( nativeDomObject ) {
|
||||
/**
|
||||
* The native DOM object represented by this class instance.
|
||||
*
|
||||
* var element = new CKEDITOR.dom.element( 'span' );
|
||||
* alert( element.$.nodeType ); // '1'
|
||||
*
|
||||
* @readonly
|
||||
* @property {Object}
|
||||
*/
|
||||
this.$ = nativeDomObject;
|
||||
}
|
||||
};
|
||||
|
||||
CKEDITOR.dom.domObject.prototype = ( function() {
|
||||
// Do not define other local variables here. We want to keep the native
|
||||
// listener closures as clean as possible.
|
||||
|
||||
var getNativeListener = function( domObject, eventName ) {
|
||||
return function( domEvent ) {
|
||||
// In FF, when reloading the page with the editor focused, it may
|
||||
// throw an error because the CKEDITOR global is not anymore
|
||||
// available. So, we check it here first. (#2923)
|
||||
if ( typeof CKEDITOR != 'undefined' )
|
||||
domObject.fire( eventName, new CKEDITOR.dom.event( domEvent ) );
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
/**
|
||||
* Gets the private `_` object which is bound to the native
|
||||
* DOM object using {@link #getCustomData}.
|
||||
*
|
||||
* var elementA = new CKEDITOR.dom.element( nativeElement );
|
||||
* elementA.getPrivate().value = 1;
|
||||
* ...
|
||||
* var elementB = new CKEDITOR.dom.element( nativeElement );
|
||||
* elementB.getPrivate().value; // 1
|
||||
*
|
||||
* @returns {Object} The private object.
|
||||
*/
|
||||
getPrivate: function() {
|
||||
var priv;
|
||||
|
||||
// Get the main private object from the custom data. Create it if not defined.
|
||||
if ( !( priv = this.getCustomData( '_' ) ) )
|
||||
this.setCustomData( '_', ( priv = {} ) );
|
||||
|
||||
return priv;
|
||||
},
|
||||
|
||||
// Docs inherited from event.
|
||||
on: function( eventName ) {
|
||||
// We customize the "on" function here. The basic idea is that we'll have
|
||||
// only one listener for a native event, which will then call all listeners
|
||||
// set to the event.
|
||||
|
||||
// Get the listeners holder object.
|
||||
var nativeListeners = this.getCustomData( '_cke_nativeListeners' );
|
||||
|
||||
if ( !nativeListeners ) {
|
||||
nativeListeners = {};
|
||||
this.setCustomData( '_cke_nativeListeners', nativeListeners );
|
||||
}
|
||||
|
||||
// Check if we have a listener for that event.
|
||||
if ( !nativeListeners[ eventName ] ) {
|
||||
var listener = nativeListeners[ eventName ] = getNativeListener( this, eventName );
|
||||
|
||||
if ( this.$.addEventListener )
|
||||
this.$.addEventListener( eventName, listener, !!CKEDITOR.event.useCapture );
|
||||
else if ( this.$.attachEvent )
|
||||
this.$.attachEvent( 'on' + eventName, listener );
|
||||
}
|
||||
|
||||
// Call the original implementation.
|
||||
return CKEDITOR.event.prototype.on.apply( this, arguments );
|
||||
},
|
||||
|
||||
// Docs inherited from event.
|
||||
removeListener: function( eventName ) {
|
||||
// Call the original implementation.
|
||||
CKEDITOR.event.prototype.removeListener.apply( this, arguments );
|
||||
|
||||
// If we don't have listeners for this event, clean the DOM up.
|
||||
if ( !this.hasListeners( eventName ) ) {
|
||||
var nativeListeners = this.getCustomData( '_cke_nativeListeners' );
|
||||
var listener = nativeListeners && nativeListeners[ eventName ];
|
||||
if ( listener ) {
|
||||
if ( this.$.removeEventListener )
|
||||
this.$.removeEventListener( eventName, listener, false );
|
||||
else if ( this.$.detachEvent )
|
||||
this.$.detachEvent( 'on' + eventName, listener );
|
||||
|
||||
delete nativeListeners[ eventName ];
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes any listener set on this object.
|
||||
*
|
||||
* To avoid memory leaks we must assure that there are no
|
||||
* references left after the object is no longer needed.
|
||||
*/
|
||||
removeAllListeners: function() {
|
||||
var nativeListeners = this.getCustomData( '_cke_nativeListeners' );
|
||||
for ( var eventName in nativeListeners ) {
|
||||
var listener = nativeListeners[ eventName ];
|
||||
if ( this.$.detachEvent )
|
||||
this.$.detachEvent( 'on' + eventName, listener );
|
||||
else if ( this.$.removeEventListener )
|
||||
this.$.removeEventListener( eventName, listener, false );
|
||||
|
||||
delete nativeListeners[ eventName ];
|
||||
}
|
||||
|
||||
// Remove events from events object so fire() method will not call
|
||||
// listeners (#11400).
|
||||
CKEDITOR.event.prototype.removeAllListeners.call( this );
|
||||
}
|
||||
};
|
||||
} )();
|
||||
|
||||
( function( domObjectProto ) {
|
||||
var customData = {};
|
||||
|
||||
CKEDITOR.on( 'reset', function() {
|
||||
customData = {};
|
||||
} );
|
||||
|
||||
/**
|
||||
* Determines whether the specified object is equal to the current object.
|
||||
*
|
||||
* var doc = new CKEDITOR.dom.document( document );
|
||||
* alert( doc.equals( CKEDITOR.document ) ); // true
|
||||
* alert( doc == CKEDITOR.document ); // false
|
||||
*
|
||||
* @param {Object} object The object to compare with the current object.
|
||||
* @returns {Boolean} `true` if the object is equal.
|
||||
*/
|
||||
domObjectProto.equals = function( object ) {
|
||||
// Try/Catch to avoid IE permission error when object is from different document.
|
||||
try {
|
||||
return ( object && object.$ === this.$ );
|
||||
} catch ( er ) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets a data slot value for this object. These values are shared by all
|
||||
* instances pointing to that same DOM object.
|
||||
*
|
||||
* **Note:** The created data slot is only guaranteed to be available on this unique DOM node,
|
||||
* thus any wish to continue access to it from other element clones (either created by
|
||||
* clone node or from `innerHtml`) will fail. For such usage please use
|
||||
* {@link CKEDITOR.dom.element#setAttribute} instead.
|
||||
*
|
||||
* **Note**: This method does not work on text nodes prior to Internet Explorer 9.
|
||||
*
|
||||
* var element = new CKEDITOR.dom.element( 'span' );
|
||||
* element.setCustomData( 'hasCustomData', true );
|
||||
*
|
||||
* @param {String} key A key used to identify the data slot.
|
||||
* @param {Object} value The value to set to the data slot.
|
||||
* @returns {CKEDITOR.dom.domObject} This DOM object instance.
|
||||
* @chainable
|
||||
*/
|
||||
domObjectProto.setCustomData = function( key, value ) {
|
||||
var expandoNumber = this.getUniqueId(),
|
||||
dataSlot = customData[ expandoNumber ] || ( customData[ expandoNumber ] = {} );
|
||||
|
||||
dataSlot[ key ] = value;
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the value set to a data slot in this object.
|
||||
*
|
||||
* var element = new CKEDITOR.dom.element( 'span' );
|
||||
* alert( element.getCustomData( 'hasCustomData' ) ); // e.g. 'true'
|
||||
* alert( element.getCustomData( 'nonExistingKey' ) ); // null
|
||||
*
|
||||
* @param {String} key The key used to identify the data slot.
|
||||
* @returns {Object} This value set to the data slot.
|
||||
*/
|
||||
domObjectProto.getCustomData = function( key ) {
|
||||
var expandoNumber = this.$[ 'data-cke-expando' ],
|
||||
dataSlot = expandoNumber && customData[ expandoNumber ];
|
||||
|
||||
return ( dataSlot && key in dataSlot ) ? dataSlot[ key ] : null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes the value in the data slot under the given `key`.
|
||||
*
|
||||
* @param {String} key
|
||||
* @returns {Object} Removed value or `null` if not found.
|
||||
*/
|
||||
domObjectProto.removeCustomData = function( key ) {
|
||||
var expandoNumber = this.$[ 'data-cke-expando' ],
|
||||
dataSlot = expandoNumber && customData[ expandoNumber ],
|
||||
retval, hadKey;
|
||||
|
||||
if ( dataSlot ) {
|
||||
retval = dataSlot[ key ];
|
||||
hadKey = key in dataSlot;
|
||||
delete dataSlot[ key ];
|
||||
}
|
||||
|
||||
return hadKey ? retval : null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes any data stored in this object.
|
||||
* To avoid memory leaks we must assure that there are no
|
||||
* references left after the object is no longer needed.
|
||||
*/
|
||||
domObjectProto.clearCustomData = function() {
|
||||
// Clear all event listeners
|
||||
this.removeAllListeners();
|
||||
|
||||
var expandoNumber = this.$[ 'data-cke-expando' ];
|
||||
expandoNumber && delete customData[ expandoNumber ];
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets an ID that can be used to identify this DOM object in
|
||||
* the running session.
|
||||
*
|
||||
* **Note**: This method does not work on text nodes prior to Internet Explorer 9.
|
||||
*
|
||||
* @returns {Number} A unique ID.
|
||||
*/
|
||||
domObjectProto.getUniqueId = function() {
|
||||
return this.$[ 'data-cke-expando' ] || ( this.$[ 'data-cke-expando' ] = CKEDITOR.tools.getNextNumber() );
|
||||
};
|
||||
|
||||
// Implement CKEDITOR.event.
|
||||
CKEDITOR.event.implementOn( domObjectProto );
|
||||
|
||||
} )( CKEDITOR.dom.domObject.prototype );
|
||||
2183
ckeditor/core/dom/element.js
Normal file
2183
ckeditor/core/dom/element.js
Normal file
File diff suppressed because it is too large
Load Diff
251
ckeditor/core/dom/elementpath.js
Normal file
251
ckeditor/core/dom/elementpath.js
Normal file
@@ -0,0 +1,251 @@
|
||||
/**
|
||||
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
|
||||
* For licensing, see LICENSE.md or http://ckeditor.com/license
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
( function() {
|
||||
|
||||
var pathBlockLimitElements = {},
|
||||
pathBlockElements = {},
|
||||
tag;
|
||||
|
||||
// Elements that are considered the "Block limit" in an element path.
|
||||
for ( tag in CKEDITOR.dtd.$blockLimit ) {
|
||||
// Exclude from list roots.
|
||||
if ( !( tag in CKEDITOR.dtd.$list ) )
|
||||
pathBlockLimitElements[ tag ] = 1;
|
||||
}
|
||||
|
||||
// Elements that are considered the "End level Block" in an element path.
|
||||
for ( tag in CKEDITOR.dtd.$block ) {
|
||||
// Exclude block limits, and empty block element, e.g. hr.
|
||||
if ( !( tag in CKEDITOR.dtd.$blockLimit || tag in CKEDITOR.dtd.$empty ) )
|
||||
pathBlockElements[ tag ] = 1;
|
||||
}
|
||||
|
||||
// Check if an element contains any block element.
|
||||
function checkHasBlock( element ) {
|
||||
var childNodes = element.getChildren();
|
||||
|
||||
for ( var i = 0, count = childNodes.count(); i < count; i++ ) {
|
||||
var child = childNodes.getItem( i );
|
||||
|
||||
if ( child.type == CKEDITOR.NODE_ELEMENT && CKEDITOR.dtd.$block[ child.getName() ] )
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the list of nodes walked from the start node up to the editable element of the editor.
|
||||
*
|
||||
* @class
|
||||
* @constructor Creates an element path class instance.
|
||||
* @param {CKEDITOR.dom.element} startNode From which the path should start.
|
||||
* @param {CKEDITOR.dom.element} root To which element the path should stop, defaults to the `body` element.
|
||||
*/
|
||||
CKEDITOR.dom.elementPath = function( startNode, root ) {
|
||||
var block = null,
|
||||
blockLimit = null,
|
||||
elements = [],
|
||||
e = startNode,
|
||||
elementName;
|
||||
|
||||
// Backward compact.
|
||||
root = root || startNode.getDocument().getBody();
|
||||
|
||||
do {
|
||||
if ( e.type == CKEDITOR.NODE_ELEMENT ) {
|
||||
elements.push( e );
|
||||
|
||||
if ( !this.lastElement ) {
|
||||
this.lastElement = e;
|
||||
|
||||
// If an object or non-editable element is fully selected at the end of the element path,
|
||||
// it must not become the block limit.
|
||||
if ( e.is( CKEDITOR.dtd.$object ) || e.getAttribute( 'contenteditable' ) == 'false' )
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( e.equals( root ) )
|
||||
break;
|
||||
|
||||
if ( !blockLimit ) {
|
||||
elementName = e.getName();
|
||||
|
||||
// First editable element becomes a block limit, because it cannot be split.
|
||||
if ( e.getAttribute( 'contenteditable' ) == 'true' )
|
||||
blockLimit = e;
|
||||
// "Else" because element cannot be both - block and block levelimit.
|
||||
else if ( !block && pathBlockElements[ elementName ] )
|
||||
block = e;
|
||||
|
||||
if ( pathBlockLimitElements[ elementName ] ) {
|
||||
// End level DIV is considered as the block, if no block is available. (#525)
|
||||
// But it must NOT be the root element (checked above).
|
||||
if ( !block && elementName == 'div' && !checkHasBlock( e ) )
|
||||
block = e;
|
||||
else
|
||||
blockLimit = e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
while ( ( e = e.getParent() ) );
|
||||
|
||||
// Block limit defaults to root.
|
||||
if ( !blockLimit )
|
||||
blockLimit = root;
|
||||
|
||||
/**
|
||||
* First non-empty block element which:
|
||||
*
|
||||
* * is not a {@link CKEDITOR.dtd#$blockLimit},
|
||||
* * or is a `div` which does not contain block elements and is not a `root`.
|
||||
*
|
||||
* This means a first, splittable block in elements path.
|
||||
*
|
||||
* @readonly
|
||||
* @property {CKEDITOR.dom.element}
|
||||
*/
|
||||
this.block = block;
|
||||
|
||||
/**
|
||||
* See the {@link CKEDITOR.dtd#$blockLimit} description.
|
||||
*
|
||||
* @readonly
|
||||
* @property {CKEDITOR.dom.element}
|
||||
*/
|
||||
this.blockLimit = blockLimit;
|
||||
|
||||
/**
|
||||
* The root of the elements path - `root` argument passed to class constructor or a `body` element.
|
||||
*
|
||||
* @readonly
|
||||
* @property {CKEDITOR.dom.element}
|
||||
*/
|
||||
this.root = root;
|
||||
|
||||
/**
|
||||
* An array of elements (from `startNode` to `root`) in the path.
|
||||
*
|
||||
* @readonly
|
||||
* @property {CKEDITOR.dom.element[]}
|
||||
*/
|
||||
this.elements = elements;
|
||||
|
||||
/**
|
||||
* The last element of the elements path - `startNode` or its parent.
|
||||
*
|
||||
* @readonly
|
||||
* @property {CKEDITOR.dom.element} lastElement
|
||||
*/
|
||||
};
|
||||
|
||||
} )();
|
||||
|
||||
CKEDITOR.dom.elementPath.prototype = {
|
||||
/**
|
||||
* Compares this element path with another one.
|
||||
*
|
||||
* @param {CKEDITOR.dom.elementPath} otherPath The elementPath object to be
|
||||
* compared with this one.
|
||||
* @returns {Boolean} `true` if the paths are equal, containing the same
|
||||
* number of elements and the same elements in the same order.
|
||||
*/
|
||||
compare: function( otherPath ) {
|
||||
var thisElements = this.elements;
|
||||
var otherElements = otherPath && otherPath.elements;
|
||||
|
||||
if ( !otherElements || thisElements.length != otherElements.length )
|
||||
return false;
|
||||
|
||||
for ( var i = 0; i < thisElements.length; i++ ) {
|
||||
if ( !thisElements[ i ].equals( otherElements[ i ] ) )
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Search the path elements that meets the specified criteria.
|
||||
*
|
||||
* @param {String/Array/Function/Object/CKEDITOR.dom.element} query The criteria that can be
|
||||
* either a tag name, list (array and object) of tag names, element or an node evaluator function.
|
||||
* @param {Boolean} [excludeRoot] Not taking path root element into consideration.
|
||||
* @param {Boolean} [fromTop] Search start from the topmost element instead of bottom.
|
||||
* @returns {CKEDITOR.dom.element} The first matched dom element or `null`.
|
||||
*/
|
||||
contains: function( query, excludeRoot, fromTop ) {
|
||||
var evaluator;
|
||||
if ( typeof query == 'string' )
|
||||
evaluator = function( node ) {
|
||||
return node.getName() == query;
|
||||
};
|
||||
if ( query instanceof CKEDITOR.dom.element )
|
||||
evaluator = function( node ) {
|
||||
return node.equals( query );
|
||||
};
|
||||
else if ( CKEDITOR.tools.isArray( query ) )
|
||||
evaluator = function( node ) {
|
||||
return CKEDITOR.tools.indexOf( query, node.getName() ) > -1;
|
||||
};
|
||||
else if ( typeof query == 'function' )
|
||||
evaluator = query;
|
||||
else if ( typeof query == 'object' )
|
||||
evaluator = function( node ) {
|
||||
return node.getName() in query;
|
||||
};
|
||||
|
||||
var elements = this.elements,
|
||||
length = elements.length;
|
||||
excludeRoot && length--;
|
||||
|
||||
if ( fromTop ) {
|
||||
elements = Array.prototype.slice.call( elements, 0 );
|
||||
elements.reverse();
|
||||
}
|
||||
|
||||
for ( var i = 0; i < length; i++ ) {
|
||||
if ( evaluator( elements[ i ] ) )
|
||||
return elements[ i ];
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Check whether the elements path is the proper context for the specified
|
||||
* tag name in the DTD.
|
||||
*
|
||||
* @param {String} tag The tag name.
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
isContextFor: function( tag ) {
|
||||
var holder;
|
||||
|
||||
// Check for block context.
|
||||
if ( tag in CKEDITOR.dtd.$block ) {
|
||||
// Indeterminate elements which are not subjected to be splitted or surrounded must be checked first.
|
||||
var inter = this.contains( CKEDITOR.dtd.$intermediate );
|
||||
holder = inter || ( this.root.equals( this.block ) && this.block ) || this.blockLimit;
|
||||
return !!holder.getDtd()[ tag ];
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve the text direction for this elements path.
|
||||
*
|
||||
* @returns {'ltr'/'rtl'}
|
||||
*/
|
||||
direction: function() {
|
||||
var directionNode = this.block || this.blockLimit || this.root;
|
||||
return directionNode.getDirection( 1 );
|
||||
}
|
||||
};
|
||||
208
ckeditor/core/dom/event.js
Normal file
208
ckeditor/core/dom/event.js
Normal file
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
|
||||
* For licensing, see LICENSE.md or http://ckeditor.com/license
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileOverview Defines the {@link CKEDITOR.dom.event} class, which
|
||||
* represents the a native DOM event object.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents a native DOM event object.
|
||||
*
|
||||
* @class
|
||||
* @constructor Creates an event class instance.
|
||||
* @param {Object} domEvent A native DOM event object.
|
||||
*/
|
||||
CKEDITOR.dom.event = function( domEvent ) {
|
||||
/**
|
||||
* The native DOM event object represented by this class instance.
|
||||
*
|
||||
* @readonly
|
||||
*/
|
||||
this.$ = domEvent;
|
||||
};
|
||||
|
||||
CKEDITOR.dom.event.prototype = {
|
||||
/**
|
||||
* Gets the key code associated to the event.
|
||||
*
|
||||
* alert( event.getKey() ); // '65' is 'a' has been pressed
|
||||
*
|
||||
* @returns {Number} The key code.
|
||||
*/
|
||||
getKey: function() {
|
||||
return this.$.keyCode || this.$.which;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets a number represeting the combination of the keys pressed during the
|
||||
* event. It is the sum with the current key code and the {@link CKEDITOR#CTRL},
|
||||
* {@link CKEDITOR#SHIFT} and {@link CKEDITOR#ALT} constants.
|
||||
*
|
||||
* alert( event.getKeystroke() == 65 ); // 'a' key
|
||||
* alert( event.getKeystroke() == CKEDITOR.CTRL + 65 ); // CTRL + 'a' key
|
||||
* alert( event.getKeystroke() == CKEDITOR.CTRL + CKEDITOR.SHIFT + 65 ); // CTRL + SHIFT + 'a' key
|
||||
*
|
||||
* @returns {Number} The number representing the keys combination.
|
||||
*/
|
||||
getKeystroke: function() {
|
||||
var keystroke = this.getKey();
|
||||
|
||||
if ( this.$.ctrlKey || this.$.metaKey )
|
||||
keystroke += CKEDITOR.CTRL;
|
||||
|
||||
if ( this.$.shiftKey )
|
||||
keystroke += CKEDITOR.SHIFT;
|
||||
|
||||
if ( this.$.altKey )
|
||||
keystroke += CKEDITOR.ALT;
|
||||
|
||||
return keystroke;
|
||||
},
|
||||
|
||||
/**
|
||||
* Prevents the original behavior of the event to happen. It can optionally
|
||||
* stop propagating the event in the event chain.
|
||||
*
|
||||
* var element = CKEDITOR.document.getById( 'myElement' );
|
||||
* element.on( 'click', function( ev ) {
|
||||
* // The DOM event object is passed by the 'data' property.
|
||||
* var domEvent = ev.data;
|
||||
* // Prevent the click to chave any effect in the element.
|
||||
* domEvent.preventDefault();
|
||||
* } );
|
||||
*
|
||||
* @param {Boolean} [stopPropagation=false] Stop propagating this event in the
|
||||
* event chain.
|
||||
*/
|
||||
preventDefault: function( stopPropagation ) {
|
||||
var $ = this.$;
|
||||
if ( $.preventDefault )
|
||||
$.preventDefault();
|
||||
else
|
||||
$.returnValue = false;
|
||||
|
||||
if ( stopPropagation )
|
||||
this.stopPropagation();
|
||||
},
|
||||
|
||||
/**
|
||||
* Stops this event propagation in the event chain.
|
||||
*/
|
||||
stopPropagation: function() {
|
||||
var $ = this.$;
|
||||
if ( $.stopPropagation )
|
||||
$.stopPropagation();
|
||||
else
|
||||
$.cancelBubble = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the DOM node where the event was targeted to.
|
||||
*
|
||||
* var element = CKEDITOR.document.getById( 'myElement' );
|
||||
* element.on( 'click', function( ev ) {
|
||||
* // The DOM event object is passed by the 'data' property.
|
||||
* var domEvent = ev.data;
|
||||
* // Add a CSS class to the event target.
|
||||
* domEvent.getTarget().addClass( 'clicked' );
|
||||
* } );
|
||||
*
|
||||
* @returns {CKEDITOR.dom.node} The target DOM node.
|
||||
*/
|
||||
getTarget: function() {
|
||||
var rawNode = this.$.target || this.$.srcElement;
|
||||
return rawNode ? new CKEDITOR.dom.node( rawNode ) : null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns an integer value that indicates the current processing phase of an event.
|
||||
* For browsers that doesn't support event phase, {@link CKEDITOR#EVENT_PHASE_AT_TARGET} is always returned.
|
||||
*
|
||||
* @returns {Number} One of {@link CKEDITOR#EVENT_PHASE_CAPTURING},
|
||||
* {@link CKEDITOR#EVENT_PHASE_AT_TARGET}, or {@link CKEDITOR#EVENT_PHASE_BUBBLING}.
|
||||
*/
|
||||
getPhase: function() {
|
||||
return this.$.eventPhase || 2;
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieves the coordinates of the mouse pointer relative to the top-left
|
||||
* corner of the document, in mouse related event.
|
||||
*
|
||||
* element.on( 'mousemouse', function( ev ) {
|
||||
* var pageOffset = ev.data.getPageOffset();
|
||||
* alert( pageOffset.x ); // page offset X
|
||||
* alert( pageOffset.y ); // page offset Y
|
||||
* } );
|
||||
*
|
||||
* @returns {Object} The object contains the position.
|
||||
* @returns {Number} return.x
|
||||
* @returns {Number} return.y
|
||||
*/
|
||||
getPageOffset: function() {
|
||||
var doc = this.getTarget().getDocument().$;
|
||||
var pageX = this.$.pageX || this.$.clientX + ( doc.documentElement.scrollLeft || doc.body.scrollLeft );
|
||||
var pageY = this.$.pageY || this.$.clientY + ( doc.documentElement.scrollTop || doc.body.scrollTop );
|
||||
return { x: pageX, y: pageY };
|
||||
}
|
||||
};
|
||||
|
||||
// For the followind constants, we need to go over the Unicode boundaries
|
||||
// (0x10FFFF) to avoid collision.
|
||||
|
||||
/**
|
||||
* CTRL key (0x110000).
|
||||
*
|
||||
* @readonly
|
||||
* @property {Number} [=0x110000]
|
||||
* @member CKEDITOR
|
||||
*/
|
||||
CKEDITOR.CTRL = 0x110000;
|
||||
|
||||
/**
|
||||
* SHIFT key (0x220000).
|
||||
*
|
||||
* @readonly
|
||||
* @property {Number} [=0x220000]
|
||||
* @member CKEDITOR
|
||||
*/
|
||||
CKEDITOR.SHIFT = 0x220000;
|
||||
|
||||
/**
|
||||
* ALT key (0x440000).
|
||||
*
|
||||
* @readonly
|
||||
* @property {Number} [=0x440000]
|
||||
* @member CKEDITOR
|
||||
*/
|
||||
CKEDITOR.ALT = 0x440000;
|
||||
|
||||
/**
|
||||
* Capturing phase.
|
||||
*
|
||||
* @readonly
|
||||
* @property {Number} [=1]
|
||||
* @member CKEDITOR
|
||||
*/
|
||||
CKEDITOR.EVENT_PHASE_CAPTURING = 1;
|
||||
|
||||
/**
|
||||
* Event at target.
|
||||
*
|
||||
* @readonly
|
||||
* @property {Number} [=2]
|
||||
* @member CKEDITOR
|
||||
*/
|
||||
CKEDITOR.EVENT_PHASE_AT_TARGET = 2;
|
||||
|
||||
/**
|
||||
* Bubbling phase.
|
||||
*
|
||||
* @readonly
|
||||
* @property {Number} [=3]
|
||||
* @member CKEDITOR
|
||||
*/
|
||||
CKEDITOR.EVENT_PHASE_BUBBLING = 3;
|
||||
565
ckeditor/core/dom/iterator.js
Normal file
565
ckeditor/core/dom/iterator.js
Normal file
@@ -0,0 +1,565 @@
|
||||
/**
|
||||
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
|
||||
* For licensing, see LICENSE.md or http://ckeditor.com/license
|
||||
*/
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
* File overview: DOM iterator which iterates over list items, lines and paragraphs.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
( function() {
|
||||
/**
|
||||
* Represents the iterator class. It can be used to iterate
|
||||
* over all elements (or even text nodes in case of {@link #enlargeBr} set to `false`)
|
||||
* which establish "paragraph-like" spaces within the passed range.
|
||||
*
|
||||
* // <h1>[foo</h1><p>bar]</p>
|
||||
* var iterator = range.createIterator();
|
||||
* iterator.getNextParagraph(); // h1 element
|
||||
* iterator.getNextParagraph(); // p element
|
||||
*
|
||||
* // <ul><li>[foo</li><li>bar]</li>
|
||||
* // With enforceRealBlocks set to false the iterator will return two list item elements.
|
||||
* // With enforceRealBlocks set to true the iterator will return two paragraphs and the DOM will be changed to:
|
||||
* // <ul><li><p>foo</p></li><li><p>bar</p></li>
|
||||
*
|
||||
* @class CKEDITOR.dom.iterator
|
||||
* @constructor Creates an iterator class instance.
|
||||
* @param {CKEDITOR.dom.range} range
|
||||
*/
|
||||
function iterator( range ) {
|
||||
if ( arguments.length < 1 )
|
||||
return;
|
||||
|
||||
/**
|
||||
* @readonly
|
||||
* @property {CKEDITOR.dom.range}
|
||||
*/
|
||||
this.range = range;
|
||||
|
||||
/**
|
||||
* @property {Boolean} [forceBrBreak=false]
|
||||
*/
|
||||
this.forceBrBreak = 0;
|
||||
|
||||
// (#3730).
|
||||
/**
|
||||
* Whether to include `<br>` elements in the enlarged range. Should be
|
||||
* set to `false` when using the iterator in the {@link CKEDITOR#ENTER_BR} mode.
|
||||
*
|
||||
* @property {Boolean} [enlargeBr=true]
|
||||
*/
|
||||
this.enlargeBr = 1;
|
||||
|
||||
/**
|
||||
* Whether the iterator should create a transformable block
|
||||
* if the current one contains text and cannot be transformed.
|
||||
* For example new blocks will be established in elements like
|
||||
* `<li>` or `<td>`.
|
||||
*
|
||||
* @property {Boolean} [enforceRealBlocks=false]
|
||||
*/
|
||||
this.enforceRealBlocks = 0;
|
||||
|
||||
this._ || ( this._ = {} );
|
||||
}
|
||||
|
||||
/**
|
||||
* Default iterator's filter. It is set only for nested iterators.
|
||||
*
|
||||
* @since 4.3
|
||||
* @readonly
|
||||
* @property {CKEDITOR.filter} filter
|
||||
*/
|
||||
|
||||
/**
|
||||
* Iterator's active filter. It is set by the {@link #getNextParagraph} method
|
||||
* when it enters a nested editable.
|
||||
*
|
||||
* @since 4.3
|
||||
* @readonly
|
||||
* @property {CKEDITOR.filter} activeFilter
|
||||
*/
|
||||
|
||||
var beginWhitespaceRegex = /^[\r\n\t ]+$/,
|
||||
// Ignore bookmark nodes.(#3783)
|
||||
bookmarkGuard = CKEDITOR.dom.walker.bookmark( false, true ),
|
||||
whitespacesGuard = CKEDITOR.dom.walker.whitespaces( true ),
|
||||
skipGuard = function( node ) {
|
||||
return bookmarkGuard( node ) && whitespacesGuard( node );
|
||||
},
|
||||
listItemNames = { dd: 1, dt: 1, li: 1 };
|
||||
|
||||
iterator.prototype = {
|
||||
/**
|
||||
* Returns the next paragraph-like element or `null` if the end of a range is reached.
|
||||
*
|
||||
* @param {String} [blockTag='p'] Name of a block element which will be established by
|
||||
* the iterator in block-less elements (see {@link #enforceRealBlocks}).
|
||||
*/
|
||||
getNextParagraph: function( blockTag ) {
|
||||
// The block element to be returned.
|
||||
var block;
|
||||
|
||||
// The range object used to identify the paragraph contents.
|
||||
var range;
|
||||
|
||||
// Indicats that the current element in the loop is the last one.
|
||||
var isLast;
|
||||
|
||||
// Instructs to cleanup remaining BRs.
|
||||
var removePreviousBr, removeLastBr;
|
||||
|
||||
blockTag = blockTag || 'p';
|
||||
|
||||
// We're iterating over nested editable.
|
||||
if ( this._.nestedEditable ) {
|
||||
// Get next block from nested iterator and returns it if was found.
|
||||
block = this._.nestedEditable.iterator.getNextParagraph( blockTag );
|
||||
if ( block ) {
|
||||
// Inherit activeFilter from the nested iterator.
|
||||
this.activeFilter = this._.nestedEditable.iterator.activeFilter;
|
||||
return block;
|
||||
}
|
||||
|
||||
// No block in nested iterator means that we reached the end of the nested editable.
|
||||
// Reset the active filter to the default filter (or undefined if this iterator didn't have it).
|
||||
this.activeFilter = this.filter;
|
||||
|
||||
// Try to find next nested editable or get back to parent (this) iterator.
|
||||
if ( startNestedEditableIterator( this, blockTag, this._.nestedEditable.container, this._.nestedEditable.remaining ) ) {
|
||||
// Inherit activeFilter from the nested iterator.
|
||||
this.activeFilter = this._.nestedEditable.iterator.activeFilter;
|
||||
return this._.nestedEditable.iterator.getNextParagraph( blockTag );
|
||||
} else {
|
||||
this._.nestedEditable = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Block-less range should be checked first.
|
||||
if ( !this.range.root.getDtd()[ blockTag ] )
|
||||
return null;
|
||||
|
||||
// This is the first iteration. Let's initialize it.
|
||||
if ( !this._.started )
|
||||
range = startIterator.call( this );
|
||||
|
||||
var currentNode = this._.nextNode,
|
||||
lastNode = this._.lastNode;
|
||||
|
||||
this._.nextNode = null;
|
||||
while ( currentNode ) {
|
||||
// closeRange indicates that a paragraph boundary has been found,
|
||||
// so the range can be closed.
|
||||
var closeRange = 0,
|
||||
parentPre = currentNode.hasAscendant( 'pre' );
|
||||
|
||||
// includeNode indicates that the current node is good to be part
|
||||
// of the range. By default, any non-element node is ok for it.
|
||||
var includeNode = ( currentNode.type != CKEDITOR.NODE_ELEMENT ),
|
||||
continueFromSibling = 0;
|
||||
|
||||
// If it is an element node, let's check if it can be part of the range.
|
||||
if ( !includeNode ) {
|
||||
var nodeName = currentNode.getName();
|
||||
|
||||
// Non-editable block was found - return it and move to processing
|
||||
// its nested editables if they exist.
|
||||
if ( CKEDITOR.dtd.$block[ nodeName ] && currentNode.getAttribute( 'contenteditable' ) == 'false' ) {
|
||||
block = currentNode;
|
||||
|
||||
// Setup iterator for first of nested editables.
|
||||
// If there's no editable, then algorithm will move to next element after current block.
|
||||
startNestedEditableIterator( this, blockTag, block );
|
||||
|
||||
// Gets us straight to the end of getParagraph() because block variable is set.
|
||||
break;
|
||||
} else if ( currentNode.isBlockBoundary( this.forceBrBreak && !parentPre && { br: 1 } ) ) {
|
||||
// <br> boundaries must be part of the range. It will
|
||||
// happen only if ForceBrBreak.
|
||||
if ( nodeName == 'br' )
|
||||
includeNode = 1;
|
||||
else if ( !range && !currentNode.getChildCount() && nodeName != 'hr' ) {
|
||||
// If we have found an empty block, and haven't started
|
||||
// the range yet, it means we must return this block.
|
||||
block = currentNode;
|
||||
isLast = currentNode.equals( lastNode );
|
||||
break;
|
||||
}
|
||||
|
||||
// The range must finish right before the boundary,
|
||||
// including possibly skipped empty spaces. (#1603)
|
||||
if ( range ) {
|
||||
range.setEndAt( currentNode, CKEDITOR.POSITION_BEFORE_START );
|
||||
|
||||
// The found boundary must be set as the next one at this
|
||||
// point. (#1717)
|
||||
if ( nodeName != 'br' ) {
|
||||
this._.nextNode = currentNode;
|
||||
}
|
||||
}
|
||||
|
||||
closeRange = 1;
|
||||
} else {
|
||||
// If we have child nodes, let's check them.
|
||||
if ( currentNode.getFirst() ) {
|
||||
// If we don't have a range yet, let's start it.
|
||||
if ( !range ) {
|
||||
range = this.range.clone();
|
||||
range.setStartAt( currentNode, CKEDITOR.POSITION_BEFORE_START );
|
||||
}
|
||||
|
||||
currentNode = currentNode.getFirst();
|
||||
continue;
|
||||
}
|
||||
includeNode = 1;
|
||||
}
|
||||
} else if ( currentNode.type == CKEDITOR.NODE_TEXT ) {
|
||||
// Ignore normal whitespaces (i.e. not including or
|
||||
// other unicode whitespaces) before/after a block node.
|
||||
if ( beginWhitespaceRegex.test( currentNode.getText() ) )
|
||||
includeNode = 0;
|
||||
}
|
||||
|
||||
// The current node is good to be part of the range and we are
|
||||
// starting a new range, initialize it first.
|
||||
if ( includeNode && !range ) {
|
||||
range = this.range.clone();
|
||||
range.setStartAt( currentNode, CKEDITOR.POSITION_BEFORE_START );
|
||||
}
|
||||
|
||||
// The last node has been found.
|
||||
isLast = ( ( !closeRange || includeNode ) && currentNode.equals( lastNode ) );
|
||||
|
||||
// If we are in an element boundary, let's check if it is time
|
||||
// to close the range, otherwise we include the parent within it.
|
||||
if ( range && !closeRange ) {
|
||||
while ( !currentNode.getNext( skipGuard ) && !isLast ) {
|
||||
var parentNode = currentNode.getParent();
|
||||
|
||||
if ( parentNode.isBlockBoundary( this.forceBrBreak && !parentPre && { br: 1 } ) ) {
|
||||
closeRange = 1;
|
||||
includeNode = 0;
|
||||
isLast = isLast || ( parentNode.equals( lastNode ) );
|
||||
// Make sure range includes bookmarks at the end of the block. (#7359)
|
||||
range.setEndAt( parentNode, CKEDITOR.POSITION_BEFORE_END );
|
||||
break;
|
||||
}
|
||||
|
||||
currentNode = parentNode;
|
||||
includeNode = 1;
|
||||
isLast = ( currentNode.equals( lastNode ) );
|
||||
continueFromSibling = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Now finally include the node.
|
||||
if ( includeNode )
|
||||
range.setEndAt( currentNode, CKEDITOR.POSITION_AFTER_END );
|
||||
|
||||
currentNode = this._getNextSourceNode( currentNode, continueFromSibling, lastNode );
|
||||
isLast = !currentNode;
|
||||
|
||||
// We have found a block boundary. Let's close the range and move out of the
|
||||
// loop.
|
||||
if ( isLast || ( closeRange && range ) )
|
||||
break;
|
||||
}
|
||||
|
||||
// Now, based on the processed range, look for (or create) the block to be returned.
|
||||
if ( !block ) {
|
||||
// If no range has been found, this is the end.
|
||||
if ( !range ) {
|
||||
this._.docEndMarker && this._.docEndMarker.remove();
|
||||
this._.nextNode = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
var startPath = new CKEDITOR.dom.elementPath( range.startContainer, range.root );
|
||||
var startBlockLimit = startPath.blockLimit,
|
||||
checkLimits = { div: 1, th: 1, td: 1 };
|
||||
block = startPath.block;
|
||||
|
||||
if ( !block && startBlockLimit && !this.enforceRealBlocks && checkLimits[ startBlockLimit.getName() ] &&
|
||||
range.checkStartOfBlock() && range.checkEndOfBlock() && !startBlockLimit.equals( range.root ) ) {
|
||||
block = startBlockLimit;
|
||||
} else if ( !block || ( this.enforceRealBlocks && block.is( listItemNames ) ) ) {
|
||||
// Create the fixed block.
|
||||
block = this.range.document.createElement( blockTag );
|
||||
|
||||
// Move the contents of the temporary range to the fixed block.
|
||||
range.extractContents().appendTo( block );
|
||||
block.trim();
|
||||
|
||||
// Insert the fixed block into the DOM.
|
||||
range.insertNode( block );
|
||||
|
||||
removePreviousBr = removeLastBr = true;
|
||||
} else if ( block.getName() != 'li' ) {
|
||||
// If the range doesn't includes the entire contents of the
|
||||
// block, we must split it, isolating the range in a dedicated
|
||||
// block.
|
||||
if ( !range.checkStartOfBlock() || !range.checkEndOfBlock() ) {
|
||||
// The resulting block will be a clone of the current one.
|
||||
block = block.clone( false );
|
||||
|
||||
// Extract the range contents, moving it to the new block.
|
||||
range.extractContents().appendTo( block );
|
||||
block.trim();
|
||||
|
||||
// Split the block. At this point, the range will be in the
|
||||
// right position for our intents.
|
||||
var splitInfo = range.splitBlock();
|
||||
|
||||
removePreviousBr = !splitInfo.wasStartOfBlock;
|
||||
removeLastBr = !splitInfo.wasEndOfBlock;
|
||||
|
||||
// Insert the new block into the DOM.
|
||||
range.insertNode( block );
|
||||
}
|
||||
} else if ( !isLast ) {
|
||||
// LIs are returned as is, with all their children (due to the
|
||||
// nested lists). But, the next node is the node right after
|
||||
// the current range, which could be an <li> child (nested
|
||||
// lists) or the next sibling <li>.
|
||||
|
||||
this._.nextNode = ( block.equals( lastNode ) ? null : this._getNextSourceNode( range.getBoundaryNodes().endNode, 1, lastNode ) );
|
||||
}
|
||||
}
|
||||
|
||||
if ( removePreviousBr ) {
|
||||
var previousSibling = block.getPrevious();
|
||||
if ( previousSibling && previousSibling.type == CKEDITOR.NODE_ELEMENT ) {
|
||||
if ( previousSibling.getName() == 'br' )
|
||||
previousSibling.remove();
|
||||
else if ( previousSibling.getLast() && previousSibling.getLast().$.nodeName.toLowerCase() == 'br' )
|
||||
previousSibling.getLast().remove();
|
||||
}
|
||||
}
|
||||
|
||||
if ( removeLastBr ) {
|
||||
var lastChild = block.getLast();
|
||||
if ( lastChild && lastChild.type == CKEDITOR.NODE_ELEMENT && lastChild.getName() == 'br' ) {
|
||||
// Remove br filler on browser which do not need it.
|
||||
if ( !CKEDITOR.env.needsBrFiller || lastChild.getPrevious( bookmarkGuard ) || lastChild.getNext( bookmarkGuard ) )
|
||||
lastChild.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Get a reference for the next element. This is important because the
|
||||
// above block can be removed or changed, so we can rely on it for the
|
||||
// next interation.
|
||||
if ( !this._.nextNode ) {
|
||||
this._.nextNode = ( isLast || block.equals( lastNode ) || !lastNode ) ? null : this._getNextSourceNode( block, 1, lastNode );
|
||||
}
|
||||
|
||||
return block;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the next element to check or `null` when the `lastNode` or the
|
||||
* {@link #range}'s {@link CKEDITOR.dom.range#root root} is reached. Bookmarks are skipped.
|
||||
*
|
||||
* @since 4.4.6
|
||||
* @private
|
||||
* @param {CKEDITOR.dom.node} node
|
||||
* @param {Boolean} startFromSibling
|
||||
* @param {CKEDITOR.dom.node} lastNode
|
||||
* @returns {CKEDITOR.dom.node}
|
||||
*/
|
||||
_getNextSourceNode: function( node, startFromSibling, lastNode ) {
|
||||
var rootNode = this.range.root,
|
||||
next;
|
||||
|
||||
// Here we are checking in guard function whether current element
|
||||
// reach lastNode(default behaviour) and root node to prevent against
|
||||
// getting out of editor instance root DOM object.
|
||||
// #12484
|
||||
function guardFunction( node ) {
|
||||
return !( node.equals( lastNode ) || node.equals( rootNode ) );
|
||||
}
|
||||
|
||||
next = node.getNextSourceNode( startFromSibling, null, guardFunction );
|
||||
while ( !bookmarkGuard( next ) ) {
|
||||
next = next.getNextSourceNode( startFromSibling, null, guardFunction );
|
||||
}
|
||||
return next;
|
||||
}
|
||||
};
|
||||
|
||||
// @context CKEDITOR.dom.iterator
|
||||
// @returns Collapsed range which will be reused when during furter processing.
|
||||
function startIterator() {
|
||||
var range = this.range.clone(),
|
||||
// Indicate at least one of the range boundaries is inside a preformat block.
|
||||
touchPre,
|
||||
|
||||
// (#12178)
|
||||
// Remember if following situation takes place:
|
||||
// * startAtInnerBoundary: <p>foo[</p>...
|
||||
// * endAtInnerBoundary: ...<p>]bar</p>
|
||||
// Because information about line break will be lost when shrinking range.
|
||||
// Note that we test only if path block exist, because we must properly shrink
|
||||
// range containing table and/or table cells.
|
||||
// Note: When range is collapsed there's no way it can be shrinked.
|
||||
// By checking if range is collapsed we also prevent #12308.
|
||||
startPath = range.startPath(),
|
||||
endPath = range.endPath(),
|
||||
startAtInnerBoundary = !range.collapsed && rangeAtInnerBlockBoundary( range, startPath.block ),
|
||||
endAtInnerBoundary = !range.collapsed && rangeAtInnerBlockBoundary( range, endPath.block, 1 );
|
||||
|
||||
// Shrink the range to exclude harmful "noises" (#4087, #4450, #5435).
|
||||
range.shrink( CKEDITOR.SHRINK_ELEMENT, true );
|
||||
|
||||
if ( startAtInnerBoundary )
|
||||
range.setStartAt( startPath.block, CKEDITOR.POSITION_BEFORE_END );
|
||||
if ( endAtInnerBoundary )
|
||||
range.setEndAt( endPath.block, CKEDITOR.POSITION_AFTER_START );
|
||||
|
||||
touchPre = range.endContainer.hasAscendant( 'pre', true ) || range.startContainer.hasAscendant( 'pre', true );
|
||||
|
||||
range.enlarge( this.forceBrBreak && !touchPre || !this.enlargeBr ? CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS : CKEDITOR.ENLARGE_BLOCK_CONTENTS );
|
||||
|
||||
if ( !range.collapsed ) {
|
||||
var walker = new CKEDITOR.dom.walker( range.clone() ),
|
||||
ignoreBookmarkTextEvaluator = CKEDITOR.dom.walker.bookmark( true, true );
|
||||
// Avoid anchor inside bookmark inner text.
|
||||
walker.evaluator = ignoreBookmarkTextEvaluator;
|
||||
this._.nextNode = walker.next();
|
||||
// TODO: It's better to have walker.reset() used here.
|
||||
walker = new CKEDITOR.dom.walker( range.clone() );
|
||||
walker.evaluator = ignoreBookmarkTextEvaluator;
|
||||
var lastNode = walker.previous();
|
||||
this._.lastNode = lastNode.getNextSourceNode( true, null, range.root );
|
||||
|
||||
// We may have an empty text node at the end of block due to [3770].
|
||||
// If that node is the lastNode, it would cause our logic to leak to the
|
||||
// next block.(#3887)
|
||||
if ( this._.lastNode && this._.lastNode.type == CKEDITOR.NODE_TEXT && !CKEDITOR.tools.trim( this._.lastNode.getText() ) && this._.lastNode.getParent().isBlockBoundary() ) {
|
||||
var testRange = this.range.clone();
|
||||
testRange.moveToPosition( this._.lastNode, CKEDITOR.POSITION_AFTER_END );
|
||||
if ( testRange.checkEndOfBlock() ) {
|
||||
var path = new CKEDITOR.dom.elementPath( testRange.endContainer, testRange.root ),
|
||||
lastBlock = path.block || path.blockLimit;
|
||||
this._.lastNode = lastBlock.getNextSourceNode( true );
|
||||
}
|
||||
}
|
||||
|
||||
// The end of document or range.root was reached, so we need a marker node inside.
|
||||
if ( !this._.lastNode || !range.root.contains( this._.lastNode ) ) {
|
||||
this._.lastNode = this._.docEndMarker = range.document.createText( '' );
|
||||
this._.lastNode.insertAfter( lastNode );
|
||||
}
|
||||
|
||||
// Let's reuse this variable.
|
||||
range = null;
|
||||
}
|
||||
|
||||
this._.started = 1;
|
||||
|
||||
return range;
|
||||
}
|
||||
|
||||
// Does a nested editables lookup inside editablesContainer.
|
||||
// If remainingEditables is set will lookup inside this array.
|
||||
// @param {CKEDITOR.dom.element} editablesContainer
|
||||
// @param {CKEDITOR.dom.element[]} [remainingEditables]
|
||||
function getNestedEditableIn( editablesContainer, remainingEditables ) {
|
||||
if ( remainingEditables == null )
|
||||
remainingEditables = findNestedEditables( editablesContainer );
|
||||
|
||||
var editable;
|
||||
|
||||
while ( ( editable = remainingEditables.shift() ) ) {
|
||||
if ( isIterableEditable( editable ) )
|
||||
return { element: editable, remaining: remainingEditables };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Checkes whether we can iterate over this editable.
|
||||
function isIterableEditable( editable ) {
|
||||
// Reject blockless editables.
|
||||
return editable.getDtd().p;
|
||||
}
|
||||
|
||||
// Finds nested editables within container. Does not return
|
||||
// editables nested in another editable (twice).
|
||||
function findNestedEditables( container ) {
|
||||
var editables = [];
|
||||
|
||||
container.forEach( function( element ) {
|
||||
if ( element.getAttribute( 'contenteditable' ) == 'true' ) {
|
||||
editables.push( element );
|
||||
return false; // Skip children.
|
||||
}
|
||||
}, CKEDITOR.NODE_ELEMENT, true );
|
||||
|
||||
return editables;
|
||||
}
|
||||
|
||||
// Looks for a first nested editable after previousEditable (if passed) and creates
|
||||
// nested iterator for it.
|
||||
function startNestedEditableIterator( parentIterator, blockTag, editablesContainer, remainingEditables ) {
|
||||
var editable = getNestedEditableIn( editablesContainer, remainingEditables );
|
||||
|
||||
if ( !editable )
|
||||
return 0;
|
||||
|
||||
var filter = CKEDITOR.filter.instances[ editable.element.data( 'cke-filter' ) ];
|
||||
|
||||
// If current editable has a filter and this filter does not allow for block tag,
|
||||
// search for next nested editable in remaining ones.
|
||||
if ( filter && !filter.check( blockTag ) )
|
||||
return startNestedEditableIterator( parentIterator, blockTag, editablesContainer, editable.remaining );
|
||||
|
||||
var range = new CKEDITOR.dom.range( editable.element );
|
||||
range.selectNodeContents( editable.element );
|
||||
|
||||
var iterator = range.createIterator();
|
||||
// This setting actually does not change anything in this case,
|
||||
// because entire range contents is selected, so there're no <br>s to be included.
|
||||
// But it seems right to copy it too.
|
||||
iterator.enlargeBr = parentIterator.enlargeBr;
|
||||
// Inherit configuration from parent iterator.
|
||||
iterator.enforceRealBlocks = parentIterator.enforceRealBlocks;
|
||||
// Set the activeFilter (which can be overriden when this iteator will start nested iterator)
|
||||
// and the default filter, which will make it possible to reset to
|
||||
// current iterator's activeFilter after leaving nested editable.
|
||||
iterator.activeFilter = iterator.filter = filter;
|
||||
|
||||
parentIterator._.nestedEditable = {
|
||||
element: editable.element,
|
||||
container: editablesContainer,
|
||||
remaining: editable.remaining,
|
||||
iterator: iterator
|
||||
};
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Checks whether range starts or ends at inner block boundary.
|
||||
// See usage comments to learn more.
|
||||
function rangeAtInnerBlockBoundary( range, block, checkEnd ) {
|
||||
if ( !block )
|
||||
return false;
|
||||
|
||||
var testRange = range.clone();
|
||||
testRange.collapse( !checkEnd );
|
||||
return testRange.checkBoundaryOfElement( block, checkEnd ? CKEDITOR.START : CKEDITOR.END );
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link CKEDITOR.dom.iterator} instance for this range.
|
||||
*
|
||||
* @member CKEDITOR.dom.range
|
||||
* @returns {CKEDITOR.dom.iterator}
|
||||
*/
|
||||
CKEDITOR.dom.range.prototype.createIterator = function() {
|
||||
return new iterator( this );
|
||||
};
|
||||
} )();
|
||||
902
ckeditor/core/dom/node.js
Normal file
902
ckeditor/core/dom/node.js
Normal file
@@ -0,0 +1,902 @@
|
||||
/**
|
||||
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
|
||||
* For licensing, see LICENSE.md or http://ckeditor.com/license
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileOverview Defines the {@link CKEDITOR.dom.node} class which is the base
|
||||
* class for classes that represent DOM nodes.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Base class for classes representing DOM nodes. This constructor may return
|
||||
* an instance of a class that inherits from this class, like
|
||||
* {@link CKEDITOR.dom.element} or {@link CKEDITOR.dom.text}.
|
||||
*
|
||||
* @class
|
||||
* @extends CKEDITOR.dom.domObject
|
||||
* @constructor Creates a node class instance.
|
||||
* @param {Object} domNode A native DOM node.
|
||||
* @see CKEDITOR.dom.element
|
||||
* @see CKEDITOR.dom.text
|
||||
*/
|
||||
CKEDITOR.dom.node = function( domNode ) {
|
||||
if ( domNode ) {
|
||||
var type =
|
||||
domNode.nodeType == CKEDITOR.NODE_DOCUMENT ? 'document' :
|
||||
domNode.nodeType == CKEDITOR.NODE_ELEMENT ? 'element' :
|
||||
domNode.nodeType == CKEDITOR.NODE_TEXT ? 'text' :
|
||||
domNode.nodeType == CKEDITOR.NODE_COMMENT ? 'comment' :
|
||||
domNode.nodeType == CKEDITOR.NODE_DOCUMENT_FRAGMENT ? 'documentFragment' :
|
||||
'domObject'; // Call the base constructor otherwise.
|
||||
|
||||
return new CKEDITOR.dom[ type ]( domNode );
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
CKEDITOR.dom.node.prototype = new CKEDITOR.dom.domObject();
|
||||
|
||||
/**
|
||||
* Element node type.
|
||||
*
|
||||
* @readonly
|
||||
* @property {Number} [=1]
|
||||
* @member CKEDITOR
|
||||
*/
|
||||
CKEDITOR.NODE_ELEMENT = 1;
|
||||
|
||||
/**
|
||||
* Document node type.
|
||||
*
|
||||
* @readonly
|
||||
* @property {Number} [=9]
|
||||
* @member CKEDITOR
|
||||
*/
|
||||
CKEDITOR.NODE_DOCUMENT = 9;
|
||||
|
||||
/**
|
||||
* Text node type.
|
||||
*
|
||||
* @readonly
|
||||
* @property {Number} [=3]
|
||||
* @member CKEDITOR
|
||||
*/
|
||||
CKEDITOR.NODE_TEXT = 3;
|
||||
|
||||
/**
|
||||
* Comment node type.
|
||||
*
|
||||
* @readonly
|
||||
* @property {Number} [=8]
|
||||
* @member CKEDITOR
|
||||
*/
|
||||
CKEDITOR.NODE_COMMENT = 8;
|
||||
|
||||
/**
|
||||
* Document fragment node type.
|
||||
*
|
||||
* @readonly
|
||||
* @property {Number} [=11]
|
||||
* @member CKEDITOR
|
||||
*/
|
||||
CKEDITOR.NODE_DOCUMENT_FRAGMENT = 11;
|
||||
|
||||
/**
|
||||
* Indicates that positions of both nodes are identical (this is the same node). See {@link CKEDITOR.dom.node#getPosition}.
|
||||
*
|
||||
* @readonly
|
||||
* @property {Number} [=0]
|
||||
* @member CKEDITOR
|
||||
*/
|
||||
CKEDITOR.POSITION_IDENTICAL = 0;
|
||||
|
||||
/**
|
||||
* Indicates that nodes are in different (detached) trees. See {@link CKEDITOR.dom.node#getPosition}.
|
||||
*
|
||||
* @readonly
|
||||
* @property {Number} [=1]
|
||||
* @member CKEDITOR
|
||||
*/
|
||||
CKEDITOR.POSITION_DISCONNECTED = 1;
|
||||
|
||||
/**
|
||||
* Indicates that the context node follows the other node. See {@link CKEDITOR.dom.node#getPosition}.
|
||||
*
|
||||
* @readonly
|
||||
* @property {Number} [=2]
|
||||
* @member CKEDITOR
|
||||
*/
|
||||
CKEDITOR.POSITION_FOLLOWING = 2;
|
||||
|
||||
/**
|
||||
* Indicates that the context node precedes the other node. See {@link CKEDITOR.dom.node#getPosition}.
|
||||
*
|
||||
* @readonly
|
||||
* @property {Number} [=4]
|
||||
* @member CKEDITOR
|
||||
*/
|
||||
CKEDITOR.POSITION_PRECEDING = 4;
|
||||
|
||||
/**
|
||||
* Indicates that the context node is a descendant of the other node. See {@link CKEDITOR.dom.node#getPosition}.
|
||||
*
|
||||
* @readonly
|
||||
* @property {Number} [=8]
|
||||
* @member CKEDITOR
|
||||
*/
|
||||
CKEDITOR.POSITION_IS_CONTAINED = 8;
|
||||
|
||||
/**
|
||||
* Indicates that the context node contains the other node. See {@link CKEDITOR.dom.node#getPosition}.
|
||||
*
|
||||
* @readonly
|
||||
* @property {Number} [=16]
|
||||
* @member CKEDITOR
|
||||
*/
|
||||
CKEDITOR.POSITION_CONTAINS = 16;
|
||||
|
||||
CKEDITOR.tools.extend( CKEDITOR.dom.node.prototype, {
|
||||
/**
|
||||
* Makes this node a child of another element.
|
||||
*
|
||||
* var p = new CKEDITOR.dom.element( 'p' );
|
||||
* var strong = new CKEDITOR.dom.element( 'strong' );
|
||||
* strong.appendTo( p );
|
||||
*
|
||||
* // Result: '<p><strong></strong></p>'.
|
||||
*
|
||||
* @param {CKEDITOR.dom.element} element The target element to which this node will be appended.
|
||||
* @returns {CKEDITOR.dom.element} The target element.
|
||||
*/
|
||||
appendTo: function( element, toStart ) {
|
||||
element.append( this, toStart );
|
||||
return element;
|
||||
},
|
||||
|
||||
/**
|
||||
* Clones this node.
|
||||
*
|
||||
* **Note**: Values set by {#setCustomData} will not be available in the clone.
|
||||
*
|
||||
* @param {Boolean} [includeChildren=false] If `true` then all node's
|
||||
* children will be cloned recursively.
|
||||
* @param {Boolean} [cloneId=false] Whether ID attributes should be cloned, too.
|
||||
* @returns {CKEDITOR.dom.node} Clone of this node.
|
||||
*/
|
||||
clone: function( includeChildren, cloneId ) {
|
||||
var $clone = this.$.cloneNode( includeChildren );
|
||||
|
||||
// The "id" attribute should never be cloned to avoid duplication.
|
||||
removeIds( $clone );
|
||||
|
||||
var node = new CKEDITOR.dom.node( $clone );
|
||||
|
||||
// On IE8 we need to fixed HTML5 node name, see details below.
|
||||
if ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 &&
|
||||
( this.type == CKEDITOR.NODE_ELEMENT || this.type == CKEDITOR.NODE_DOCUMENT_FRAGMENT ) ) {
|
||||
renameNodes( node );
|
||||
}
|
||||
|
||||
return node;
|
||||
|
||||
function removeIds( node ) {
|
||||
// Reset data-cke-expando only when has been cloned (IE and only for some types of objects).
|
||||
if ( node[ 'data-cke-expando' ] )
|
||||
node[ 'data-cke-expando' ] = false;
|
||||
|
||||
if ( node.nodeType != CKEDITOR.NODE_ELEMENT && node.nodeType != CKEDITOR.NODE_DOCUMENT_FRAGMENT )
|
||||
return;
|
||||
|
||||
if ( !cloneId && node.nodeType == CKEDITOR.NODE_ELEMENT )
|
||||
node.removeAttribute( 'id', false );
|
||||
|
||||
if ( includeChildren ) {
|
||||
var childs = node.childNodes;
|
||||
for ( var i = 0; i < childs.length; i++ )
|
||||
removeIds( childs[ i ] );
|
||||
}
|
||||
}
|
||||
|
||||
// IE8 rename HTML5 nodes by adding `:` at the begging of the tag name when the node is cloned,
|
||||
// so `<figure>` will be `<:figure>` after 'cloneNode'. We need to fix it (#13101).
|
||||
function renameNodes( node ) {
|
||||
if ( node.type != CKEDITOR.NODE_ELEMENT && node.type != CKEDITOR.NODE_DOCUMENT_FRAGMENT )
|
||||
return;
|
||||
|
||||
if ( node.type != CKEDITOR.NODE_DOCUMENT_FRAGMENT ) {
|
||||
var name = node.getName();
|
||||
if ( name[ 0 ] == ':' ) {
|
||||
node.renameNode( name.substring( 1 ) );
|
||||
}
|
||||
}
|
||||
|
||||
if ( includeChildren ) {
|
||||
for ( var i = 0; i < node.getChildCount(); i++ )
|
||||
renameNodes( node.getChild( i ) );
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if the node is preceded by any sibling.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
hasPrevious: function() {
|
||||
return !!this.$.previousSibling;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if the node is succeeded by any sibling.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
hasNext: function() {
|
||||
return !!this.$.nextSibling;
|
||||
},
|
||||
|
||||
/**
|
||||
* Inserts this element after a node.
|
||||
*
|
||||
* var em = new CKEDITOR.dom.element( 'em' );
|
||||
* var strong = new CKEDITOR.dom.element( 'strong' );
|
||||
* strong.insertAfter( em );
|
||||
*
|
||||
* // Result: '<em></em><strong></strong>'
|
||||
*
|
||||
* @param {CKEDITOR.dom.node} node The node that will precede this element.
|
||||
* @returns {CKEDITOR.dom.node} The node preceding this one after insertion.
|
||||
*/
|
||||
insertAfter: function( node ) {
|
||||
node.$.parentNode.insertBefore( this.$, node.$.nextSibling );
|
||||
return node;
|
||||
},
|
||||
|
||||
/**
|
||||
* Inserts this element before a node.
|
||||
*
|
||||
* var em = new CKEDITOR.dom.element( 'em' );
|
||||
* var strong = new CKEDITOR.dom.element( 'strong' );
|
||||
* strong.insertBefore( em );
|
||||
*
|
||||
* // result: '<strong></strong><em></em>'
|
||||
*
|
||||
* @param {CKEDITOR.dom.node} node The node that will succeed this element.
|
||||
* @returns {CKEDITOR.dom.node} The node being inserted.
|
||||
*/
|
||||
insertBefore: function( node ) {
|
||||
node.$.parentNode.insertBefore( this.$, node.$ );
|
||||
return node;
|
||||
},
|
||||
|
||||
/**
|
||||
* Inserts a node before this node.
|
||||
*
|
||||
* var em = new CKEDITOR.dom.element( 'em' );
|
||||
* var strong = new CKEDITOR.dom.element( 'strong' );
|
||||
* strong.insertBeforeMe( em );
|
||||
*
|
||||
* // result: '<em></em><strong></strong>'
|
||||
*
|
||||
* @param {CKEDITOR.dom.node} node The node that will preceed this element.
|
||||
* @returns {CKEDITOR.dom.node} The node being inserted.
|
||||
*/
|
||||
insertBeforeMe: function( node ) {
|
||||
this.$.parentNode.insertBefore( node.$, this.$ );
|
||||
return node;
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieves a uniquely identifiable tree address for this node.
|
||||
* The tree address returned is an array of integers, with each integer
|
||||
* indicating a child index of a DOM node, starting from
|
||||
* `document.documentElement`.
|
||||
*
|
||||
* For example, assuming `<body>` is the second child
|
||||
* of `<html>` (`<head>` being the first),
|
||||
* and we would like to address the third child under the
|
||||
* fourth child of `<body>`, the tree address returned would be:
|
||||
* `[1, 3, 2]`.
|
||||
*
|
||||
* The tree address cannot be used for finding back the DOM tree node once
|
||||
* the DOM tree structure has been modified.
|
||||
*
|
||||
* @param {Boolean} [normalized=false] See {@link #getIndex}.
|
||||
* @returns {Array} The address.
|
||||
*/
|
||||
getAddress: function( normalized ) {
|
||||
var address = [];
|
||||
var $documentElement = this.getDocument().$.documentElement;
|
||||
var node = this.$;
|
||||
|
||||
while ( node && node != $documentElement ) {
|
||||
var parentNode = node.parentNode;
|
||||
|
||||
if ( parentNode ) {
|
||||
// Get the node index. For performance, call getIndex
|
||||
// directly, instead of creating a new node object.
|
||||
address.unshift( this.getIndex.call( { $: node }, normalized ) );
|
||||
}
|
||||
|
||||
node = parentNode;
|
||||
}
|
||||
|
||||
return address;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the document containing this element.
|
||||
*
|
||||
* var element = CKEDITOR.document.getById( 'example' );
|
||||
* alert( element.getDocument().equals( CKEDITOR.document ) ); // true
|
||||
*
|
||||
* @returns {CKEDITOR.dom.document} The document.
|
||||
*/
|
||||
getDocument: function() {
|
||||
return new CKEDITOR.dom.document( this.$.ownerDocument || this.$.parentNode.ownerDocument );
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the index of a node in an array of its `parent.childNodes`.
|
||||
* Returns `-1` if a node does not have a parent or when the `normalized` argument is set to `true`
|
||||
* and the text node is empty and will be removed during the normalization.
|
||||
*
|
||||
* Let us assume having the following `childNodes` array:
|
||||
*
|
||||
* [ emptyText, element1, text, text, element2, emptyText2 ]
|
||||
*
|
||||
* emptyText.getIndex() // 0
|
||||
* emptyText.getIndex( true ) // -1
|
||||
* element1.getIndex(); // 1
|
||||
* element1.getIndex( true ); // 0
|
||||
* element2.getIndex(); // 4
|
||||
* element2.getIndex( true ); // 2
|
||||
* emptyText2.getIndex(); // 5
|
||||
* emptyText2.getIndex( true ); // -1
|
||||
*
|
||||
* @param {Boolean} normalized When `true`, adjacent text nodes are merged and empty text nodes are removed.
|
||||
* @returns {Number} Index of a node or `-1` if a node does not have a parent or is removed during the normalization.
|
||||
*/
|
||||
getIndex: function( normalized ) {
|
||||
// Attention: getAddress depends on this.$
|
||||
// getIndex is called on a plain object: { $ : node }
|
||||
|
||||
var current = this.$,
|
||||
index = -1,
|
||||
isNormalizing;
|
||||
|
||||
if ( !this.$.parentNode )
|
||||
return -1;
|
||||
|
||||
// The idea is - all empty text nodes will be virtually merged into their adjacent text nodes.
|
||||
// If an empty text node does not have an adjacent non-empty text node we can return -1 straight away,
|
||||
// because it and all its sibling text nodes will be merged into an empty text node and then totally ignored.
|
||||
if ( normalized && current.nodeType == CKEDITOR.NODE_TEXT && isEmpty( current ) ) {
|
||||
var adjacent = getAdjacentNonEmptyTextNode( current ) || getAdjacentNonEmptyTextNode( current, true );
|
||||
|
||||
if ( !adjacent )
|
||||
return -1;
|
||||
}
|
||||
|
||||
do {
|
||||
// Bypass blank node and adjacent text nodes.
|
||||
if ( normalized && current != this.$ && current.nodeType == CKEDITOR.NODE_TEXT && ( isNormalizing || isEmpty( current ) ) )
|
||||
continue;
|
||||
|
||||
index++;
|
||||
isNormalizing = current.nodeType == CKEDITOR.NODE_TEXT;
|
||||
}
|
||||
while ( ( current = current.previousSibling ) );
|
||||
|
||||
return index;
|
||||
|
||||
function getAdjacentNonEmptyTextNode( node, lookForward ) {
|
||||
var sibling = lookForward ? node.nextSibling : node.previousSibling;
|
||||
|
||||
if ( !sibling || sibling.nodeType != CKEDITOR.NODE_TEXT ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If found a non-empty text node, then return it.
|
||||
// If not, then continue search.
|
||||
return isEmpty( sibling ) ? getAdjacentNonEmptyTextNode( sibling, lookForward ) : sibling;
|
||||
}
|
||||
|
||||
// Checks whether a text node is empty or is FCSeq string (which will be totally removed when normalizing).
|
||||
function isEmpty( textNode ) {
|
||||
return !textNode.nodeValue || textNode.nodeValue == CKEDITOR.dom.selection.FILLING_CHAR_SEQUENCE;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @todo
|
||||
*/
|
||||
getNextSourceNode: function( startFromSibling, nodeType, guard ) {
|
||||
// If "guard" is a node, transform it in a function.
|
||||
if ( guard && !guard.call ) {
|
||||
var guardNode = guard;
|
||||
guard = function( node ) {
|
||||
return !node.equals( guardNode );
|
||||
};
|
||||
}
|
||||
|
||||
var node = ( !startFromSibling && this.getFirst && this.getFirst() ),
|
||||
parent;
|
||||
|
||||
// Guarding when we're skipping the current element( no children or 'startFromSibling' ).
|
||||
// send the 'moving out' signal even we don't actually dive into.
|
||||
if ( !node ) {
|
||||
if ( this.type == CKEDITOR.NODE_ELEMENT && guard && guard( this, true ) === false )
|
||||
return null;
|
||||
node = this.getNext();
|
||||
}
|
||||
|
||||
while ( !node && ( parent = ( parent || this ).getParent() ) ) {
|
||||
// The guard check sends the "true" paramenter to indicate that
|
||||
// we are moving "out" of the element.
|
||||
if ( guard && guard( parent, true ) === false )
|
||||
return null;
|
||||
|
||||
node = parent.getNext();
|
||||
}
|
||||
|
||||
if ( !node )
|
||||
return null;
|
||||
|
||||
if ( guard && guard( node ) === false )
|
||||
return null;
|
||||
|
||||
if ( nodeType && nodeType != node.type )
|
||||
return node.getNextSourceNode( false, nodeType, guard );
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
/**
|
||||
* @todo
|
||||
*/
|
||||
getPreviousSourceNode: function( startFromSibling, nodeType, guard ) {
|
||||
if ( guard && !guard.call ) {
|
||||
var guardNode = guard;
|
||||
guard = function( node ) {
|
||||
return !node.equals( guardNode );
|
||||
};
|
||||
}
|
||||
|
||||
var node = ( !startFromSibling && this.getLast && this.getLast() ),
|
||||
parent;
|
||||
|
||||
// Guarding when we're skipping the current element( no children or 'startFromSibling' ).
|
||||
// send the 'moving out' signal even we don't actually dive into.
|
||||
if ( !node ) {
|
||||
if ( this.type == CKEDITOR.NODE_ELEMENT && guard && guard( this, true ) === false )
|
||||
return null;
|
||||
node = this.getPrevious();
|
||||
}
|
||||
|
||||
while ( !node && ( parent = ( parent || this ).getParent() ) ) {
|
||||
// The guard check sends the "true" paramenter to indicate that
|
||||
// we are moving "out" of the element.
|
||||
if ( guard && guard( parent, true ) === false )
|
||||
return null;
|
||||
|
||||
node = parent.getPrevious();
|
||||
}
|
||||
|
||||
if ( !node )
|
||||
return null;
|
||||
|
||||
if ( guard && guard( node ) === false )
|
||||
return null;
|
||||
|
||||
if ( nodeType && node.type != nodeType )
|
||||
return node.getPreviousSourceNode( false, nodeType, guard );
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the node that preceeds this element in its parent's child list.
|
||||
*
|
||||
* var element = CKEDITOR.dom.element.createFromHtml( '<div><i>prev</i><b>Example</b></div>' );
|
||||
* var first = element.getLast().getPrev();
|
||||
* alert( first.getName() ); // 'i'
|
||||
*
|
||||
* @param {Function} [evaluator] Filtering the result node.
|
||||
* @returns {CKEDITOR.dom.node} The previous node or null if not available.
|
||||
*/
|
||||
getPrevious: function( evaluator ) {
|
||||
var previous = this.$,
|
||||
retval;
|
||||
do {
|
||||
previous = previous.previousSibling;
|
||||
|
||||
// Avoid returning the doc type node.
|
||||
// http://www.w3.org/TR/REC-DOM-Level-1/level-one-core.html#ID-412266927
|
||||
retval = previous && previous.nodeType != 10 && new CKEDITOR.dom.node( previous );
|
||||
}
|
||||
while ( retval && evaluator && !evaluator( retval ) );
|
||||
return retval;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the node that follows this element in its parent's child list.
|
||||
*
|
||||
* var element = CKEDITOR.dom.element.createFromHtml( '<div><b>Example</b><i>next</i></div>' );
|
||||
* var last = element.getFirst().getNext();
|
||||
* alert( last.getName() ); // 'i'
|
||||
*
|
||||
* @param {Function} [evaluator] Filtering the result node.
|
||||
* @returns {CKEDITOR.dom.node} The next node or null if not available.
|
||||
*/
|
||||
getNext: function( evaluator ) {
|
||||
var next = this.$,
|
||||
retval;
|
||||
do {
|
||||
next = next.nextSibling;
|
||||
retval = next && new CKEDITOR.dom.node( next );
|
||||
}
|
||||
while ( retval && evaluator && !evaluator( retval ) );
|
||||
return retval;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the parent element for this node.
|
||||
*
|
||||
* var node = editor.document.getBody().getFirst();
|
||||
* var parent = node.getParent();
|
||||
* alert( parent.getName() ); // 'body'
|
||||
*
|
||||
* @param {Boolean} [allowFragmentParent=false] Consider also parent node that is of
|
||||
* fragment type {@link CKEDITOR#NODE_DOCUMENT_FRAGMENT}.
|
||||
* @returns {CKEDITOR.dom.element} The parent element.
|
||||
*/
|
||||
getParent: function( allowFragmentParent ) {
|
||||
var parent = this.$.parentNode;
|
||||
return ( parent && ( parent.nodeType == CKEDITOR.NODE_ELEMENT || allowFragmentParent && parent.nodeType == CKEDITOR.NODE_DOCUMENT_FRAGMENT ) ) ? new CKEDITOR.dom.node( parent ) : null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns an array containing node parents and the node itself. By default nodes are in _descending_ order.
|
||||
*
|
||||
* // Assuming that body has paragraph as the first child.
|
||||
* var node = editor.document.getBody().getFirst();
|
||||
* var parents = node.getParents();
|
||||
* alert( parents[ 0 ].getName() + ',' + parents[ 2 ].getName() ); // 'html,p'
|
||||
*
|
||||
* @param {Boolean} [closerFirst=false] Determines the order of returned nodes.
|
||||
* @returns {Array} Returns an array of {@link CKEDITOR.dom.node}.
|
||||
*/
|
||||
getParents: function( closerFirst ) {
|
||||
var node = this;
|
||||
var parents = [];
|
||||
|
||||
do {
|
||||
parents[ closerFirst ? 'push' : 'unshift' ]( node );
|
||||
}
|
||||
while ( ( node = node.getParent() ) );
|
||||
|
||||
return parents;
|
||||
},
|
||||
|
||||
/**
|
||||
* @todo
|
||||
*/
|
||||
getCommonAncestor: function( node ) {
|
||||
if ( node.equals( this ) )
|
||||
return this;
|
||||
|
||||
if ( node.contains && node.contains( this ) )
|
||||
return node;
|
||||
|
||||
var start = this.contains ? this : this.getParent();
|
||||
|
||||
do {
|
||||
if ( start.contains( node ) ) return start;
|
||||
}
|
||||
while ( ( start = start.getParent() ) );
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Determines the position relation between this node and the given {@link CKEDITOR.dom.node} in the document.
|
||||
* This node can be preceding ({@link CKEDITOR#POSITION_PRECEDING}) or following ({@link CKEDITOR#POSITION_FOLLOWING})
|
||||
* the given node. This node can also contain ({@link CKEDITOR#POSITION_CONTAINS}) or be contained by
|
||||
* ({@link CKEDITOR#POSITION_IS_CONTAINED}) the given node. The function returns a bitmask of constants
|
||||
* listed above or {@link CKEDITOR#POSITION_IDENTICAL} if the given node is the same as this node.
|
||||
*
|
||||
* @param {CKEDITOR.dom.node} otherNode A node to check relation with.
|
||||
* @returns {Number} Position relation between this node and given node.
|
||||
*/
|
||||
getPosition: function( otherNode ) {
|
||||
var $ = this.$;
|
||||
var $other = otherNode.$;
|
||||
|
||||
if ( $.compareDocumentPosition )
|
||||
return $.compareDocumentPosition( $other );
|
||||
|
||||
// IE and Safari have no support for compareDocumentPosition.
|
||||
|
||||
if ( $ == $other )
|
||||
return CKEDITOR.POSITION_IDENTICAL;
|
||||
|
||||
// Only element nodes support contains and sourceIndex.
|
||||
if ( this.type == CKEDITOR.NODE_ELEMENT && otherNode.type == CKEDITOR.NODE_ELEMENT ) {
|
||||
if ( $.contains ) {
|
||||
if ( $.contains( $other ) )
|
||||
return CKEDITOR.POSITION_CONTAINS + CKEDITOR.POSITION_PRECEDING;
|
||||
|
||||
if ( $other.contains( $ ) )
|
||||
return CKEDITOR.POSITION_IS_CONTAINED + CKEDITOR.POSITION_FOLLOWING;
|
||||
}
|
||||
|
||||
if ( 'sourceIndex' in $ )
|
||||
return ( $.sourceIndex < 0 || $other.sourceIndex < 0 ) ? CKEDITOR.POSITION_DISCONNECTED : ( $.sourceIndex < $other.sourceIndex ) ? CKEDITOR.POSITION_PRECEDING : CKEDITOR.POSITION_FOLLOWING;
|
||||
|
||||
}
|
||||
|
||||
// For nodes that don't support compareDocumentPosition, contains
|
||||
// or sourceIndex, their "address" is compared.
|
||||
|
||||
var addressOfThis = this.getAddress(),
|
||||
addressOfOther = otherNode.getAddress(),
|
||||
minLevel = Math.min( addressOfThis.length, addressOfOther.length );
|
||||
|
||||
// Determinate preceding/following relationship.
|
||||
for ( var i = 0; i < minLevel; i++ ) {
|
||||
if ( addressOfThis[ i ] != addressOfOther[ i ] ) {
|
||||
return addressOfThis[ i ] < addressOfOther[ i ] ? CKEDITOR.POSITION_PRECEDING : CKEDITOR.POSITION_FOLLOWING;
|
||||
}
|
||||
}
|
||||
|
||||
// Determinate contains/contained relationship.
|
||||
return ( addressOfThis.length < addressOfOther.length ) ? CKEDITOR.POSITION_CONTAINS + CKEDITOR.POSITION_PRECEDING : CKEDITOR.POSITION_IS_CONTAINED + CKEDITOR.POSITION_FOLLOWING;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the closest ancestor node of this node, specified by its name or using an evaluator function.
|
||||
*
|
||||
* // Suppose we have the following HTML structure:
|
||||
* // <div id="outer"><div id="inner"><p><b>Some text</b></p></div></div>
|
||||
* // If node == <b>
|
||||
* ascendant = node.getAscendant( 'div' ); // ascendant == <div id="inner">
|
||||
* ascendant = node.getAscendant( 'b' ); // ascendant == null
|
||||
* ascendant = node.getAscendant( 'b', true ); // ascendant == <b>
|
||||
* ascendant = node.getAscendant( { div:1, p:1 } ); // Searches for the first 'div' or 'p': ascendant == <div id="inner">
|
||||
*
|
||||
* // Using custom evaluator:
|
||||
* ascendant = node.getAscendant( function( el ) {
|
||||
* return el.getId() == 'inner';
|
||||
* } );
|
||||
* // ascendant == <div id="inner">
|
||||
*
|
||||
* @since 3.6.1
|
||||
* @param {String/Function/Object} query The name of the ancestor node to search or
|
||||
* an object with the node names to search for or an evaluator function.
|
||||
* @param {Boolean} [includeSelf] Whether to include the current
|
||||
* node in the search.
|
||||
* @returns {CKEDITOR.dom.node} The located ancestor node or `null` if not found.
|
||||
*/
|
||||
getAscendant: function( query, includeSelf ) {
|
||||
var $ = this.$,
|
||||
evaluator,
|
||||
isCustomEvaluator;
|
||||
|
||||
if ( !includeSelf ) {
|
||||
$ = $.parentNode;
|
||||
}
|
||||
|
||||
// Custom checker provided in an argument.
|
||||
if ( typeof query == 'function' ) {
|
||||
isCustomEvaluator = true;
|
||||
evaluator = query;
|
||||
} else {
|
||||
// Predefined tag name checker.
|
||||
isCustomEvaluator = false;
|
||||
evaluator = function( $ ) {
|
||||
var name = ( typeof $.nodeName == 'string' ? $.nodeName.toLowerCase() : '' );
|
||||
|
||||
return ( typeof query == 'string' ? name == query : name in query );
|
||||
};
|
||||
}
|
||||
|
||||
while ( $ ) {
|
||||
// For user provided checker we use CKEDITOR.dom.node.
|
||||
if ( evaluator( isCustomEvaluator ? new CKEDITOR.dom.node( $ ) : $ ) ) {
|
||||
return new CKEDITOR.dom.node( $ );
|
||||
}
|
||||
|
||||
try {
|
||||
$ = $.parentNode;
|
||||
} catch ( e ) {
|
||||
$ = null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* @todo
|
||||
*/
|
||||
hasAscendant: function( name, includeSelf ) {
|
||||
var $ = this.$;
|
||||
|
||||
if ( !includeSelf )
|
||||
$ = $.parentNode;
|
||||
|
||||
while ( $ ) {
|
||||
if ( $.nodeName && $.nodeName.toLowerCase() == name )
|
||||
return true;
|
||||
|
||||
$ = $.parentNode;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* @todo
|
||||
*/
|
||||
move: function( target, toStart ) {
|
||||
target.append( this.remove(), toStart );
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes this node from the document DOM.
|
||||
*
|
||||
* var element = CKEDITOR.document.getById( 'MyElement' );
|
||||
* element.remove();
|
||||
*
|
||||
* @param {Boolean} [preserveChildren=false] Indicates that the children
|
||||
* elements must remain in the document, removing only the outer tags.
|
||||
*/
|
||||
remove: function( preserveChildren ) {
|
||||
var $ = this.$;
|
||||
var parent = $.parentNode;
|
||||
|
||||
if ( parent ) {
|
||||
if ( preserveChildren ) {
|
||||
// Move all children before the node.
|
||||
for ( var child;
|
||||
( child = $.firstChild ); ) {
|
||||
parent.insertBefore( $.removeChild( child ), $ );
|
||||
}
|
||||
}
|
||||
|
||||
parent.removeChild( $ );
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* @todo
|
||||
*/
|
||||
replace: function( nodeToReplace ) {
|
||||
this.insertBefore( nodeToReplace );
|
||||
nodeToReplace.remove();
|
||||
},
|
||||
|
||||
/**
|
||||
* @todo
|
||||
*/
|
||||
trim: function() {
|
||||
this.ltrim();
|
||||
this.rtrim();
|
||||
},
|
||||
|
||||
/**
|
||||
* @todo
|
||||
*/
|
||||
ltrim: function() {
|
||||
var child;
|
||||
while ( this.getFirst && ( child = this.getFirst() ) ) {
|
||||
if ( child.type == CKEDITOR.NODE_TEXT ) {
|
||||
var trimmed = CKEDITOR.tools.ltrim( child.getText() ),
|
||||
originalLength = child.getLength();
|
||||
|
||||
if ( !trimmed ) {
|
||||
child.remove();
|
||||
continue;
|
||||
} else if ( trimmed.length < originalLength ) {
|
||||
child.split( originalLength - trimmed.length );
|
||||
|
||||
// IE BUG: child.remove() may raise JavaScript errors here. (#81)
|
||||
this.$.removeChild( this.$.firstChild );
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @todo
|
||||
*/
|
||||
rtrim: function() {
|
||||
var child;
|
||||
while ( this.getLast && ( child = this.getLast() ) ) {
|
||||
if ( child.type == CKEDITOR.NODE_TEXT ) {
|
||||
var trimmed = CKEDITOR.tools.rtrim( child.getText() ),
|
||||
originalLength = child.getLength();
|
||||
|
||||
if ( !trimmed ) {
|
||||
child.remove();
|
||||
continue;
|
||||
} else if ( trimmed.length < originalLength ) {
|
||||
child.split( trimmed.length );
|
||||
|
||||
// IE BUG: child.getNext().remove() may raise JavaScript errors here.
|
||||
// (#81)
|
||||
this.$.lastChild.parentNode.removeChild( this.$.lastChild );
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if ( CKEDITOR.env.needsBrFiller ) {
|
||||
child = this.$.lastChild;
|
||||
|
||||
if ( child && child.type == 1 && child.nodeName.toLowerCase() == 'br' ) {
|
||||
// Use "eChildNode.parentNode" instead of "node" to avoid IE bug (#324).
|
||||
child.parentNode.removeChild( child );
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if this node is read-only (should not be changed).
|
||||
*
|
||||
* // For the following HTML:
|
||||
* // <b>foo</b><div contenteditable="false"><i>bar</i></div>
|
||||
*
|
||||
* elB.isReadOnly(); // -> false
|
||||
* foo.isReadOnly(); // -> false
|
||||
* elDiv.isReadOnly(); // -> true
|
||||
* elI.isReadOnly(); // -> true
|
||||
*
|
||||
* This method works in two modes depending on browser support for the `element.isContentEditable` property and
|
||||
* the value of the `checkOnlyAttributes` parameter. The `element.isContentEditable` check is faster, but it is known
|
||||
* to malfunction in hidden or detached nodes. Additionally, when processing some detached DOM tree you may want to imitate
|
||||
* that this happens inside an editable container (like it would happen inside the {@link CKEDITOR.editable}). To do so,
|
||||
* you can temporarily attach this tree to an element with the `data-cke-editable` attribute and use the
|
||||
* `checkOnlyAttributes` mode.
|
||||
*
|
||||
* @since 3.5
|
||||
* @param {Boolean} [checkOnlyAttributes=false] If `true`, only attributes will be checked, native methods will not
|
||||
* be used. This parameter needs to be `true` to check hidden or detached elements. Introduced in 4.5.
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
isReadOnly: function( checkOnlyAttributes ) {
|
||||
var element = this;
|
||||
if ( this.type != CKEDITOR.NODE_ELEMENT )
|
||||
element = this.getParent();
|
||||
|
||||
// Prevent Edge crash (#13609, #13919).
|
||||
if ( CKEDITOR.env.edge && element && element.is( 'textarea', 'input' ) ) {
|
||||
checkOnlyAttributes = true;
|
||||
}
|
||||
|
||||
if ( !checkOnlyAttributes && element && typeof element.$.isContentEditable != 'undefined' ) {
|
||||
return !( element.$.isContentEditable || element.data( 'cke-editable' ) );
|
||||
}
|
||||
else {
|
||||
// Degrade for old browsers which don't support "isContentEditable", e.g. FF3
|
||||
|
||||
while ( element ) {
|
||||
if ( element.data( 'cke-editable' ) ) {
|
||||
return false;
|
||||
} else if ( element.hasAttribute( 'contenteditable' ) ) {
|
||||
return element.getAttribute( 'contenteditable' ) == 'false';
|
||||
}
|
||||
|
||||
element = element.getParent();
|
||||
}
|
||||
|
||||
// Reached the root of DOM tree, no editable found.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} );
|
||||
43
ckeditor/core/dom/nodelist.js
Normal file
43
ckeditor/core/dom/nodelist.js
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
|
||||
* For licensing, see LICENSE.md or http://ckeditor.com/license
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents a list of {@link CKEDITOR.dom.node} objects.
|
||||
* It's a wrapper for native nodes list.
|
||||
*
|
||||
* var nodeList = CKEDITOR.document.getBody().getChildren();
|
||||
* alert( nodeList.count() ); // number [0;N]
|
||||
*
|
||||
* @class
|
||||
* @constructor Creates a document class instance.
|
||||
* @param {Object} nativeList
|
||||
*/
|
||||
CKEDITOR.dom.nodeList = function( nativeList ) {
|
||||
this.$ = nativeList;
|
||||
};
|
||||
|
||||
CKEDITOR.dom.nodeList.prototype = {
|
||||
/**
|
||||
* Get count of nodes in this list.
|
||||
*
|
||||
* @returns {Number}
|
||||
*/
|
||||
count: function() {
|
||||
return this.$.length;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get node from the list.
|
||||
*
|
||||
* @returns {CKEDITOR.dom.node}
|
||||
*/
|
||||
getItem: function( index ) {
|
||||
if ( index < 0 || index >= this.$.length )
|
||||
return null;
|
||||
|
||||
var $node = this.$[ index ];
|
||||
return $node ? new CKEDITOR.dom.node( $node ) : null;
|
||||
}
|
||||
};
|
||||
2978
ckeditor/core/dom/range.js
Normal file
2978
ckeditor/core/dom/range.js
Normal file
File diff suppressed because it is too large
Load Diff
199
ckeditor/core/dom/rangelist.js
Normal file
199
ckeditor/core/dom/rangelist.js
Normal file
@@ -0,0 +1,199 @@
|
||||
/**
|
||||
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
|
||||
* For licensing, see LICENSE.md or http://ckeditor.com/license
|
||||
*/
|
||||
|
||||
( function() {
|
||||
/**
|
||||
* Represents a list os CKEDITOR.dom.range objects, which can be easily
|
||||
* iterated sequentially.
|
||||
*
|
||||
* @class
|
||||
* @extends Array
|
||||
* @constructor Creates a rangeList class instance.
|
||||
* @param {CKEDITOR.dom.range/CKEDITOR.dom.range[]} [ranges] The ranges contained on this list.
|
||||
* Note that, if an array of ranges is specified, the range sequence
|
||||
* should match its DOM order. This class will not help to sort them.
|
||||
*/
|
||||
CKEDITOR.dom.rangeList = function( ranges ) {
|
||||
if ( ranges instanceof CKEDITOR.dom.rangeList )
|
||||
return ranges;
|
||||
|
||||
if ( !ranges )
|
||||
ranges = [];
|
||||
else if ( ranges instanceof CKEDITOR.dom.range )
|
||||
ranges = [ ranges ];
|
||||
|
||||
return CKEDITOR.tools.extend( ranges, mixins );
|
||||
};
|
||||
|
||||
var mixins = {
|
||||
/**
|
||||
* Creates an instance of the rangeList iterator, it should be used
|
||||
* only when the ranges processing could be DOM intrusive, which
|
||||
* means it may pollute and break other ranges in this list.
|
||||
* Otherwise, it's enough to just iterate over this array in a for loop.
|
||||
*
|
||||
* @returns {CKEDITOR.dom.rangeListIterator}
|
||||
*/
|
||||
createIterator: function() {
|
||||
var rangeList = this,
|
||||
bookmark = CKEDITOR.dom.walker.bookmark(),
|
||||
bookmarks = [],
|
||||
current;
|
||||
|
||||
return {
|
||||
/**
|
||||
* Retrieves the next range in the list.
|
||||
*
|
||||
* @member CKEDITOR.dom.rangeListIterator
|
||||
* @param {Boolean} [mergeConsequent=false] Whether join two adjacent
|
||||
* ranges into single, e.g. consequent table cells.
|
||||
*/
|
||||
getNextRange: function( mergeConsequent ) {
|
||||
current = current === undefined ? 0 : current + 1;
|
||||
|
||||
var range = rangeList[ current ];
|
||||
|
||||
// Multiple ranges might be mangled by each other.
|
||||
if ( range && rangeList.length > 1 ) {
|
||||
// Bookmarking all other ranges on the first iteration,
|
||||
// the range correctness after it doesn't matter since we'll
|
||||
// restore them before the next iteration.
|
||||
if ( !current ) {
|
||||
// Make sure bookmark correctness by reverse processing.
|
||||
for ( var i = rangeList.length - 1; i >= 0; i-- )
|
||||
bookmarks.unshift( rangeList[ i ].createBookmark( true ) );
|
||||
}
|
||||
|
||||
if ( mergeConsequent ) {
|
||||
// Figure out how many ranges should be merged.
|
||||
var mergeCount = 0;
|
||||
while ( rangeList[ current + mergeCount + 1 ] ) {
|
||||
var doc = range.document,
|
||||
found = 0,
|
||||
left = doc.getById( bookmarks[ mergeCount ].endNode ),
|
||||
right = doc.getById( bookmarks[ mergeCount + 1 ].startNode ),
|
||||
next;
|
||||
|
||||
// Check subsequent range.
|
||||
while ( 1 ) {
|
||||
next = left.getNextSourceNode( false );
|
||||
if ( !right.equals( next ) ) {
|
||||
// This could be yet another bookmark or
|
||||
// walking across block boundaries.
|
||||
if ( bookmark( next ) || ( next.type == CKEDITOR.NODE_ELEMENT && next.isBlockBoundary() ) ) {
|
||||
left = next;
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
found = 1;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if ( !found )
|
||||
break;
|
||||
|
||||
mergeCount++;
|
||||
}
|
||||
}
|
||||
|
||||
range.moveToBookmark( bookmarks.shift() );
|
||||
|
||||
// Merge ranges finally after moving to bookmarks.
|
||||
while ( mergeCount-- ) {
|
||||
next = rangeList[ ++current ];
|
||||
next.moveToBookmark( bookmarks.shift() );
|
||||
range.setEnd( next.endContainer, next.endOffset );
|
||||
}
|
||||
}
|
||||
|
||||
return range;
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Create bookmarks for all ranges. See {@link CKEDITOR.dom.range#createBookmark}.
|
||||
*
|
||||
* @param {Boolean} [serializable=false] See {@link CKEDITOR.dom.range#createBookmark}.
|
||||
* @returns {Array} Array of bookmarks.
|
||||
*/
|
||||
createBookmarks: function( serializable ) {
|
||||
var retval = [],
|
||||
bookmark;
|
||||
for ( var i = 0; i < this.length; i++ ) {
|
||||
retval.push( bookmark = this[ i ].createBookmark( serializable, true ) );
|
||||
|
||||
// Updating the container & offset values for ranges
|
||||
// that have been touched.
|
||||
for ( var j = i + 1; j < this.length; j++ ) {
|
||||
this[ j ] = updateDirtyRange( bookmark, this[ j ] );
|
||||
this[ j ] = updateDirtyRange( bookmark, this[ j ], true );
|
||||
}
|
||||
}
|
||||
return retval;
|
||||
},
|
||||
|
||||
/**
|
||||
* Create "unobtrusive" bookmarks for all ranges. See {@link CKEDITOR.dom.range#createBookmark2}.
|
||||
*
|
||||
* @param {Boolean} [normalized=false] See {@link CKEDITOR.dom.range#createBookmark2}.
|
||||
* @returns {Array} Array of bookmarks.
|
||||
*/
|
||||
createBookmarks2: function( normalized ) {
|
||||
var bookmarks = [];
|
||||
|
||||
for ( var i = 0; i < this.length; i++ )
|
||||
bookmarks.push( this[ i ].createBookmark2( normalized ) );
|
||||
|
||||
return bookmarks;
|
||||
},
|
||||
|
||||
/**
|
||||
* Move each range in the list to the position specified by a list of bookmarks.
|
||||
*
|
||||
* @param {Array} bookmarks The list of bookmarks, each one matching a range in the list.
|
||||
*/
|
||||
moveToBookmarks: function( bookmarks ) {
|
||||
for ( var i = 0; i < this.length; i++ )
|
||||
this[ i ].moveToBookmark( bookmarks[ i ] );
|
||||
}
|
||||
};
|
||||
|
||||
// Update the specified range which has been mangled by previous insertion of
|
||||
// range bookmark nodes.(#3256)
|
||||
function updateDirtyRange( bookmark, dirtyRange, checkEnd ) {
|
||||
var serializable = bookmark.serializable,
|
||||
container = dirtyRange[ checkEnd ? 'endContainer' : 'startContainer' ],
|
||||
offset = checkEnd ? 'endOffset' : 'startOffset';
|
||||
|
||||
var bookmarkStart = serializable ? dirtyRange.document.getById( bookmark.startNode ) : bookmark.startNode;
|
||||
|
||||
var bookmarkEnd = serializable ? dirtyRange.document.getById( bookmark.endNode ) : bookmark.endNode;
|
||||
|
||||
if ( container.equals( bookmarkStart.getPrevious() ) ) {
|
||||
dirtyRange.startOffset = dirtyRange.startOffset - container.getLength() - bookmarkEnd.getPrevious().getLength();
|
||||
container = bookmarkEnd.getNext();
|
||||
} else if ( container.equals( bookmarkEnd.getPrevious() ) ) {
|
||||
dirtyRange.startOffset = dirtyRange.startOffset - container.getLength();
|
||||
container = bookmarkEnd.getNext();
|
||||
}
|
||||
|
||||
container.equals( bookmarkStart.getParent() ) && dirtyRange[ offset ]++;
|
||||
container.equals( bookmarkEnd.getParent() ) && dirtyRange[ offset ]++;
|
||||
|
||||
// Update and return this range.
|
||||
dirtyRange[ checkEnd ? 'endContainer' : 'startContainer' ] = container;
|
||||
return dirtyRange;
|
||||
}
|
||||
} )();
|
||||
|
||||
/**
|
||||
* (Virtual Class) Do not call this constructor. This class is not really part
|
||||
* of the API. It just describes the return type of {@link CKEDITOR.dom.rangeList#createIterator}.
|
||||
*
|
||||
* @class CKEDITOR.dom.rangeListIterator
|
||||
*/
|
||||
135
ckeditor/core/dom/text.js
Normal file
135
ckeditor/core/dom/text.js
Normal file
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
|
||||
* For licensing, see LICENSE.md or http://ckeditor.com/license
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileOverview Defines the {@link CKEDITOR.dom.text} class, which represents
|
||||
* a DOM text node.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents a DOM text node.
|
||||
*
|
||||
* var nativeNode = document.createTextNode( 'Example' );
|
||||
* var text = new CKEDITOR.dom.text( nativeNode );
|
||||
*
|
||||
* var text = new CKEDITOR.dom.text( 'Example' );
|
||||
*
|
||||
* @class
|
||||
* @extends CKEDITOR.dom.node
|
||||
* @constructor Creates a text class instance.
|
||||
* @param {Object/String} text A native DOM text node or a string containing
|
||||
* the text to use to create a new text node.
|
||||
* @param {CKEDITOR.dom.document} [ownerDocument] The document that will contain
|
||||
* the node in case of new node creation. Defaults to the current document.
|
||||
*/
|
||||
CKEDITOR.dom.text = function( text, ownerDocument ) {
|
||||
if ( typeof text == 'string' )
|
||||
text = ( ownerDocument ? ownerDocument.$ : document ).createTextNode( text );
|
||||
|
||||
// Theoretically, we should call the base constructor here
|
||||
// (not CKEDITOR.dom.node though). But, IE doesn't support expando
|
||||
// properties on text node, so the features provided by domObject will not
|
||||
// work for text nodes (which is not a big issue for us).
|
||||
//
|
||||
// CKEDITOR.dom.domObject.call( this, element );
|
||||
|
||||
this.$ = text;
|
||||
};
|
||||
|
||||
CKEDITOR.dom.text.prototype = new CKEDITOR.dom.node();
|
||||
|
||||
CKEDITOR.tools.extend( CKEDITOR.dom.text.prototype, {
|
||||
/**
|
||||
* The node type. This is a constant value set to {@link CKEDITOR#NODE_TEXT}.
|
||||
*
|
||||
* @readonly
|
||||
* @property {Number} [=CKEDITOR.NODE_TEXT]
|
||||
*/
|
||||
type: CKEDITOR.NODE_TEXT,
|
||||
|
||||
/**
|
||||
* Gets length of node's value.
|
||||
*
|
||||
* @returns {Number}
|
||||
*/
|
||||
getLength: function() {
|
||||
return this.$.nodeValue.length;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets node's value.
|
||||
*
|
||||
* @returns {String}
|
||||
*/
|
||||
getText: function() {
|
||||
return this.$.nodeValue;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets node's value.
|
||||
*
|
||||
* @param {String} text
|
||||
*/
|
||||
setText: function( text ) {
|
||||
this.$.nodeValue = text;
|
||||
},
|
||||
|
||||
/**
|
||||
* Breaks this text node into two nodes at the specified offset,
|
||||
* keeping both in the tree as siblings. This node then only contains
|
||||
* all the content up to the offset point. A new text node, which is
|
||||
* inserted as the next sibling of this node, contains all the content
|
||||
* at and after the offset point. When the offset is equal to the
|
||||
* length of this node, the new node has no data.
|
||||
*
|
||||
* @param {Number} The position at which to split, starting from zero.
|
||||
* @returns {CKEDITOR.dom.text} The new text node.
|
||||
*/
|
||||
split: function( offset ) {
|
||||
|
||||
// Saved the children count and text length beforehand.
|
||||
var parent = this.$.parentNode,
|
||||
count = parent.childNodes.length,
|
||||
length = this.getLength();
|
||||
|
||||
var doc = this.getDocument();
|
||||
var retval = new CKEDITOR.dom.text( this.$.splitText( offset ), doc );
|
||||
|
||||
if ( parent.childNodes.length == count ) {
|
||||
// If the offset is after the last char, IE creates the text node
|
||||
// on split, but don't include it into the DOM. So, we have to do
|
||||
// that manually here.
|
||||
if ( offset >= length ) {
|
||||
retval = doc.createText( '' );
|
||||
retval.insertAfter( this );
|
||||
} else {
|
||||
// IE BUG: IE8+ does not update the childNodes array in DOM after splitText(),
|
||||
// we need to make some DOM changes to make it update. (#3436)
|
||||
var workaround = doc.createText( '' );
|
||||
workaround.insertAfter( retval );
|
||||
workaround.remove();
|
||||
}
|
||||
}
|
||||
|
||||
return retval;
|
||||
},
|
||||
|
||||
/**
|
||||
* Extracts characters from indexA up to but not including `indexB`.
|
||||
*
|
||||
* @param {Number} indexA An integer between `0` and one less than the
|
||||
* length of the text.
|
||||
* @param {Number} [indexB] An integer between `0` and the length of the
|
||||
* string. If omitted, extracts characters to the end of the text.
|
||||
*/
|
||||
substring: function( indexA, indexB ) {
|
||||
// We need the following check due to a Firefox bug
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=458886
|
||||
if ( typeof indexB != 'number' )
|
||||
return this.$.nodeValue.substr( indexA );
|
||||
else
|
||||
return this.$.nodeValue.substring( indexA, indexB );
|
||||
}
|
||||
} );
|
||||
652
ckeditor/core/dom/walker.js
Normal file
652
ckeditor/core/dom/walker.js
Normal file
@@ -0,0 +1,652 @@
|
||||
/**
|
||||
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
|
||||
* For licensing, see LICENSE.md or http://ckeditor.com/license
|
||||
*/
|
||||
|
||||
( function() {
|
||||
// This function is to be called under a "walker" instance scope.
|
||||
function iterate( rtl, breakOnFalse ) {
|
||||
var range = this.range;
|
||||
|
||||
// Return null if we have reached the end.
|
||||
if ( this._.end )
|
||||
return null;
|
||||
|
||||
// This is the first call. Initialize it.
|
||||
if ( !this._.start ) {
|
||||
this._.start = 1;
|
||||
|
||||
// A collapsed range must return null at first call.
|
||||
if ( range.collapsed ) {
|
||||
this.end();
|
||||
return null;
|
||||
}
|
||||
|
||||
// Move outside of text node edges.
|
||||
range.optimize();
|
||||
}
|
||||
|
||||
var node,
|
||||
startCt = range.startContainer,
|
||||
endCt = range.endContainer,
|
||||
startOffset = range.startOffset,
|
||||
endOffset = range.endOffset,
|
||||
guard,
|
||||
userGuard = this.guard,
|
||||
type = this.type,
|
||||
getSourceNodeFn = ( rtl ? 'getPreviousSourceNode' : 'getNextSourceNode' );
|
||||
|
||||
// Create the LTR guard function, if necessary.
|
||||
if ( !rtl && !this._.guardLTR ) {
|
||||
// The node that stops walker from moving up.
|
||||
var limitLTR = endCt.type == CKEDITOR.NODE_ELEMENT ? endCt : endCt.getParent();
|
||||
|
||||
// The node that stops the walker from going to next.
|
||||
var blockerLTR = endCt.type == CKEDITOR.NODE_ELEMENT ? endCt.getChild( endOffset ) : endCt.getNext();
|
||||
|
||||
this._.guardLTR = function( node, movingOut ) {
|
||||
return ( ( !movingOut || !limitLTR.equals( node ) ) && ( !blockerLTR || !node.equals( blockerLTR ) ) && ( node.type != CKEDITOR.NODE_ELEMENT || !movingOut || !node.equals( range.root ) ) );
|
||||
};
|
||||
}
|
||||
|
||||
// Create the RTL guard function, if necessary.
|
||||
if ( rtl && !this._.guardRTL ) {
|
||||
// The node that stops walker from moving up.
|
||||
var limitRTL = startCt.type == CKEDITOR.NODE_ELEMENT ? startCt : startCt.getParent();
|
||||
|
||||
// The node that stops the walker from going to next.
|
||||
var blockerRTL = startCt.type == CKEDITOR.NODE_ELEMENT ? startOffset ? startCt.getChild( startOffset - 1 ) : null : startCt.getPrevious();
|
||||
|
||||
this._.guardRTL = function( node, movingOut ) {
|
||||
return ( ( !movingOut || !limitRTL.equals( node ) ) && ( !blockerRTL || !node.equals( blockerRTL ) ) && ( node.type != CKEDITOR.NODE_ELEMENT || !movingOut || !node.equals( range.root ) ) );
|
||||
};
|
||||
}
|
||||
|
||||
// Define which guard function to use.
|
||||
var stopGuard = rtl ? this._.guardRTL : this._.guardLTR;
|
||||
|
||||
// Make the user defined guard function participate in the process,
|
||||
// otherwise simply use the boundary guard.
|
||||
if ( userGuard ) {
|
||||
guard = function( node, movingOut ) {
|
||||
if ( stopGuard( node, movingOut ) === false )
|
||||
return false;
|
||||
|
||||
return userGuard( node, movingOut );
|
||||
};
|
||||
} else {
|
||||
guard = stopGuard;
|
||||
}
|
||||
|
||||
if ( this.current )
|
||||
node = this.current[ getSourceNodeFn ]( false, type, guard );
|
||||
else {
|
||||
// Get the first node to be returned.
|
||||
if ( rtl ) {
|
||||
node = endCt;
|
||||
|
||||
if ( node.type == CKEDITOR.NODE_ELEMENT ) {
|
||||
if ( endOffset > 0 )
|
||||
node = node.getChild( endOffset - 1 );
|
||||
else
|
||||
node = ( guard( node, true ) === false ) ? null : node.getPreviousSourceNode( true, type, guard );
|
||||
}
|
||||
} else {
|
||||
node = startCt;
|
||||
|
||||
if ( node.type == CKEDITOR.NODE_ELEMENT ) {
|
||||
if ( !( node = node.getChild( startOffset ) ) )
|
||||
node = ( guard( startCt, true ) === false ) ? null : startCt.getNextSourceNode( true, type, guard );
|
||||
}
|
||||
}
|
||||
|
||||
if ( node && guard( node ) === false )
|
||||
node = null;
|
||||
}
|
||||
|
||||
while ( node && !this._.end ) {
|
||||
this.current = node;
|
||||
|
||||
if ( !this.evaluator || this.evaluator( node ) !== false ) {
|
||||
if ( !breakOnFalse )
|
||||
return node;
|
||||
} else if ( breakOnFalse && this.evaluator ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
node = node[ getSourceNodeFn ]( false, type, guard );
|
||||
}
|
||||
|
||||
this.end();
|
||||
return this.current = null;
|
||||
}
|
||||
|
||||
function iterateToLast( rtl ) {
|
||||
var node,
|
||||
last = null;
|
||||
|
||||
while ( ( node = iterate.call( this, rtl ) ) )
|
||||
last = node;
|
||||
|
||||
return last;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility class to "walk" the DOM inside range boundaries. If the
|
||||
* range starts or ends in the middle of the text node, this node will
|
||||
* be included as a whole. Outside changes to the range may break the walker.
|
||||
*
|
||||
* The walker may return nodes that are not totally included in the
|
||||
* range boundaries. Let us take the following range representation,
|
||||
* where the square brackets indicate the boundaries:
|
||||
*
|
||||
* [<p>Some <b>sample] text</b>
|
||||
*
|
||||
* While walking forward into the above range, the following nodes are
|
||||
* returned: `<p>`, `"Some "`, `<b>` and `"sample"`. Going
|
||||
* backwards instead we have: `"sample"` and `"Some "`. So note that the
|
||||
* walker always returns nodes when "entering" them, but not when
|
||||
* "leaving" them. The {@link #guard} function is instead called both when
|
||||
* entering and when leaving nodes.
|
||||
*
|
||||
* @class
|
||||
*/
|
||||
CKEDITOR.dom.walker = CKEDITOR.tools.createClass( {
|
||||
/**
|
||||
* Creates a walker class instance.
|
||||
*
|
||||
* @constructor
|
||||
* @param {CKEDITOR.dom.range} range The range within which to walk.
|
||||
*/
|
||||
$: function( range ) {
|
||||
this.range = range;
|
||||
|
||||
/**
|
||||
* A function executed for every matched node to check whether
|
||||
* it is to be considered in the walk or not. If not provided, all
|
||||
* matched nodes are considered good.
|
||||
*
|
||||
* If the function returns `false`, the node is ignored.
|
||||
*
|
||||
* @property {Function} evaluator
|
||||
*/
|
||||
// this.evaluator = null;
|
||||
|
||||
/**
|
||||
* A function executed for every node the walk passes by to check
|
||||
* whether the walk is to be finished. It is called both when
|
||||
* entering and when exiting nodes, as well as for the matched nodes.
|
||||
*
|
||||
* If this function returns `false`, the walking ends and no more
|
||||
* nodes are evaluated.
|
||||
|
||||
* @property {Function} guard
|
||||
*/
|
||||
// this.guard = null;
|
||||
|
||||
/** @private */
|
||||
this._ = {};
|
||||
},
|
||||
|
||||
// statics :
|
||||
// {
|
||||
// /* Creates a CKEDITOR.dom.walker instance to walk inside DOM boundaries set by nodes.
|
||||
// * @param {CKEDITOR.dom.node} startNode The node from which the walk
|
||||
// * will start.
|
||||
// * @param {CKEDITOR.dom.node} [endNode] The last node to be considered
|
||||
// * in the walk. No more nodes are retrieved after touching or
|
||||
// * passing it. If not provided, the walker stops at the
|
||||
// * <body> closing boundary.
|
||||
// * @returns {CKEDITOR.dom.walker} A DOM walker for the nodes between the
|
||||
// * provided nodes.
|
||||
// */
|
||||
// createOnNodes : function( startNode, endNode, startInclusive, endInclusive )
|
||||
// {
|
||||
// var range = new CKEDITOR.dom.range();
|
||||
// if ( startNode )
|
||||
// range.setStartAt( startNode, startInclusive ? CKEDITOR.POSITION_BEFORE_START : CKEDITOR.POSITION_AFTER_END ) ;
|
||||
// else
|
||||
// range.setStartAt( startNode.getDocument().getBody(), CKEDITOR.POSITION_AFTER_START ) ;
|
||||
//
|
||||
// if ( endNode )
|
||||
// range.setEndAt( endNode, endInclusive ? CKEDITOR.POSITION_AFTER_END : CKEDITOR.POSITION_BEFORE_START ) ;
|
||||
// else
|
||||
// range.setEndAt( startNode.getDocument().getBody(), CKEDITOR.POSITION_BEFORE_END ) ;
|
||||
//
|
||||
// return new CKEDITOR.dom.walker( range );
|
||||
// }
|
||||
// },
|
||||
//
|
||||
proto: {
|
||||
/**
|
||||
* Stops walking. No more nodes are retrieved if this function is called.
|
||||
*/
|
||||
end: function() {
|
||||
this._.end = 1;
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieves the next node (on the right).
|
||||
*
|
||||
* @returns {CKEDITOR.dom.node} The next node or `null` if no more
|
||||
* nodes are available.
|
||||
*/
|
||||
next: function() {
|
||||
return iterate.call( this );
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieves the previous node (on the left).
|
||||
*
|
||||
* @returns {CKEDITOR.dom.node} The previous node or `null` if no more
|
||||
* nodes are available.
|
||||
*/
|
||||
previous: function() {
|
||||
return iterate.call( this, 1 );
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks all nodes on the right, executing the evaluation function.
|
||||
*
|
||||
* @returns {Boolean} `false` if the evaluator function returned
|
||||
* `false` for any of the matched nodes. Otherwise `true`.
|
||||
*/
|
||||
checkForward: function() {
|
||||
return iterate.call( this, 0, 1 ) !== false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Check all nodes on the left, executing the evaluation function.
|
||||
*
|
||||
* @returns {Boolean} `false` if the evaluator function returned
|
||||
* `false` for any of the matched nodes. Otherwise `true`.
|
||||
*/
|
||||
checkBackward: function() {
|
||||
return iterate.call( this, 1, 1 ) !== false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Executes a full walk forward (to the right), until no more nodes
|
||||
* are available, returning the last valid node.
|
||||
*
|
||||
* @returns {CKEDITOR.dom.node} The last node on the right or `null`
|
||||
* if no valid nodes are available.
|
||||
*/
|
||||
lastForward: function() {
|
||||
return iterateToLast.call( this );
|
||||
},
|
||||
|
||||
/**
|
||||
* Executes a full walk backwards (to the left), until no more nodes
|
||||
* are available, returning the last valid node.
|
||||
*
|
||||
* @returns {CKEDITOR.dom.node} The last node on the left or `null`
|
||||
* if no valid nodes are available.
|
||||
*/
|
||||
lastBackward: function() {
|
||||
return iterateToLast.call( this, 1 );
|
||||
},
|
||||
|
||||
/**
|
||||
* Resets the walker.
|
||||
*/
|
||||
reset: function() {
|
||||
delete this.current;
|
||||
this._ = {};
|
||||
}
|
||||
|
||||
}
|
||||
} );
|
||||
|
||||
// Anything whose display computed style is block, list-item, table,
|
||||
// table-row-group, table-header-group, table-footer-group, table-row,
|
||||
// table-column-group, table-column, table-cell, table-caption, or whose node
|
||||
// name is hr, br (when enterMode is br only) is a block boundary.
|
||||
var blockBoundaryDisplayMatch = {
|
||||
block: 1, 'list-item': 1, table: 1, 'table-row-group': 1,
|
||||
'table-header-group': 1, 'table-footer-group': 1, 'table-row': 1, 'table-column-group': 1,
|
||||
'table-column': 1, 'table-cell': 1, 'table-caption': 1
|
||||
},
|
||||
outOfFlowPositions = { absolute: 1, fixed: 1 };
|
||||
|
||||
/**
|
||||
* Checks whether an element is displayed as a block.
|
||||
*
|
||||
* @member CKEDITOR.dom.element
|
||||
* @param [customNodeNames] Custom list of nodes which will extend
|
||||
* the default {@link CKEDITOR.dtd#$block} list.
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
CKEDITOR.dom.element.prototype.isBlockBoundary = function( customNodeNames ) {
|
||||
// Whether element is in normal page flow. Floated or positioned elements are out of page flow.
|
||||
// Don't consider floated or positioned formatting as block boundary, fall back to dtd check in that case. (#6297)
|
||||
var inPageFlow = this.getComputedStyle( 'float' ) == 'none' && !( this.getComputedStyle( 'position' ) in outOfFlowPositions );
|
||||
|
||||
if ( inPageFlow && blockBoundaryDisplayMatch[ this.getComputedStyle( 'display' ) ] )
|
||||
return true;
|
||||
|
||||
// Either in $block or in customNodeNames if defined.
|
||||
return !!( this.is( CKEDITOR.dtd.$block ) || customNodeNames && this.is( customNodeNames ) );
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a function which checks whether the node is a block boundary.
|
||||
* See {@link CKEDITOR.dom.element#isBlockBoundary}.
|
||||
*
|
||||
* @static
|
||||
* @param customNodeNames
|
||||
* @returns {Function}
|
||||
*/
|
||||
CKEDITOR.dom.walker.blockBoundary = function( customNodeNames ) {
|
||||
return function( node ) {
|
||||
return !( node.type == CKEDITOR.NODE_ELEMENT && node.isBlockBoundary( customNodeNames ) );
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @static
|
||||
* @todo
|
||||
*/
|
||||
CKEDITOR.dom.walker.listItemBoundary = function() {
|
||||
return this.blockBoundary( { br: 1 } );
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a function which checks whether the node is a bookmark node or the bookmark node
|
||||
* inner content.
|
||||
*
|
||||
* @static
|
||||
* @param {Boolean} [contentOnly=false] Whether only test against the text content of
|
||||
* a bookmark node instead of the element itself (default).
|
||||
* @param {Boolean} [isReject=false] Whether to return `false` for the bookmark
|
||||
* node instead of `true` (default).
|
||||
* @returns {Function}
|
||||
*/
|
||||
CKEDITOR.dom.walker.bookmark = function( contentOnly, isReject ) {
|
||||
function isBookmarkNode( node ) {
|
||||
return ( node && node.getName && node.getName() == 'span' && node.data( 'cke-bookmark' ) );
|
||||
}
|
||||
|
||||
return function( node ) {
|
||||
var isBookmark, parent;
|
||||
// Is bookmark inner text node?
|
||||
isBookmark = ( node && node.type != CKEDITOR.NODE_ELEMENT && ( parent = node.getParent() ) && isBookmarkNode( parent ) );
|
||||
// Is bookmark node?
|
||||
isBookmark = contentOnly ? isBookmark : isBookmark || isBookmarkNode( node );
|
||||
return !!( isReject ^ isBookmark );
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a function which checks whether the node is a text node containing only whitespace characters.
|
||||
*
|
||||
* @static
|
||||
* @param {Boolean} [isReject=false]
|
||||
* @returns {Function}
|
||||
*/
|
||||
CKEDITOR.dom.walker.whitespaces = function( isReject ) {
|
||||
return function( node ) {
|
||||
var isWhitespace;
|
||||
if ( node && node.type == CKEDITOR.NODE_TEXT ) {
|
||||
// Whitespace, as well as the Filling Char Sequence text node used in Webkit. (#9384, #13816)
|
||||
isWhitespace = !CKEDITOR.tools.trim( node.getText() ) ||
|
||||
CKEDITOR.env.webkit && node.getText() == CKEDITOR.dom.selection.FILLING_CHAR_SEQUENCE;
|
||||
}
|
||||
|
||||
return !!( isReject ^ isWhitespace );
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a function which checks whether the node is invisible in the WYSIWYG mode.
|
||||
*
|
||||
* @static
|
||||
* @param {Boolean} [isReject=false]
|
||||
* @returns {Function}
|
||||
*/
|
||||
CKEDITOR.dom.walker.invisible = function( isReject ) {
|
||||
var whitespace = CKEDITOR.dom.walker.whitespaces(),
|
||||
// #12221 (Chrome) plus #11111 (Safari).
|
||||
offsetWidth0 = CKEDITOR.env.webkit ? 1 : 0;
|
||||
|
||||
return function( node ) {
|
||||
var invisible;
|
||||
|
||||
if ( whitespace( node ) )
|
||||
invisible = 1;
|
||||
else {
|
||||
// Visibility should be checked on element.
|
||||
if ( node.type == CKEDITOR.NODE_TEXT )
|
||||
node = node.getParent();
|
||||
|
||||
// Nodes that take no spaces in wysiwyg:
|
||||
// 1. White-spaces but not including NBSP.
|
||||
// 2. Empty inline elements, e.g. <b></b>.
|
||||
// 3. <br> elements (bogus, surrounded by text) (#12423).
|
||||
invisible = node.$.offsetWidth <= offsetWidth0;
|
||||
}
|
||||
|
||||
return !!( isReject ^ invisible );
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a function which checks whether the node type is equal to the passed one.
|
||||
*
|
||||
* @static
|
||||
* @param {Number} type
|
||||
* @param {Boolean} [isReject=false]
|
||||
* @returns {Function}
|
||||
*/
|
||||
CKEDITOR.dom.walker.nodeType = function( type, isReject ) {
|
||||
return function( node ) {
|
||||
return !!( isReject ^ ( node.type == type ) );
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a function which checks whether the node is a bogus (filler) node from
|
||||
* `contenteditable` element's point of view.
|
||||
*
|
||||
* @static
|
||||
* @param {Boolean} [isReject=false]
|
||||
* @returns {Function}
|
||||
*/
|
||||
CKEDITOR.dom.walker.bogus = function( isReject ) {
|
||||
function nonEmpty( node ) {
|
||||
return !isWhitespaces( node ) && !isBookmark( node );
|
||||
}
|
||||
|
||||
return function( node ) {
|
||||
var isBogus = CKEDITOR.env.needsBrFiller ? node.is && node.is( 'br' ) : node.getText && tailNbspRegex.test( node.getText() );
|
||||
|
||||
if ( isBogus ) {
|
||||
var parent = node.getParent(),
|
||||
next = node.getNext( nonEmpty );
|
||||
|
||||
isBogus = parent.isBlockBoundary() && ( !next || next.type == CKEDITOR.NODE_ELEMENT && next.isBlockBoundary() );
|
||||
}
|
||||
|
||||
return !!( isReject ^ isBogus );
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a function which checks whether the node is a temporary element
|
||||
* (element with the `data-cke-temp` attribute) or its child.
|
||||
*
|
||||
* @since 4.3
|
||||
* @static
|
||||
* @param {Boolean} [isReject=false] Whether to return `false` for the
|
||||
* temporary element instead of `true` (default).
|
||||
* @returns {Function}
|
||||
*/
|
||||
CKEDITOR.dom.walker.temp = function( isReject ) {
|
||||
return function( node ) {
|
||||
if ( node.type != CKEDITOR.NODE_ELEMENT )
|
||||
node = node.getParent();
|
||||
|
||||
var isTemp = node && node.hasAttribute( 'data-cke-temp' );
|
||||
|
||||
return !!( isReject ^ isTemp );
|
||||
};
|
||||
};
|
||||
|
||||
var tailNbspRegex = /^[\t\r\n ]*(?: |\xa0)$/,
|
||||
isWhitespaces = CKEDITOR.dom.walker.whitespaces(),
|
||||
isBookmark = CKEDITOR.dom.walker.bookmark(),
|
||||
isTemp = CKEDITOR.dom.walker.temp(),
|
||||
toSkip = function( node ) {
|
||||
return isBookmark( node ) ||
|
||||
isWhitespaces( node ) ||
|
||||
node.type == CKEDITOR.NODE_ELEMENT && node.is( CKEDITOR.dtd.$inline ) && !node.is( CKEDITOR.dtd.$empty );
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a function which checks whether the node should be ignored in terms of "editability".
|
||||
*
|
||||
* This includes:
|
||||
*
|
||||
* * whitespaces (see {@link CKEDITOR.dom.walker#whitespaces}),
|
||||
* * bookmarks (see {@link CKEDITOR.dom.walker#bookmark}),
|
||||
* * temporary elements (see {@link CKEDITOR.dom.walker#temp}).
|
||||
*
|
||||
* @since 4.3
|
||||
* @static
|
||||
* @param {Boolean} [isReject=false] Whether to return `false` for the
|
||||
* ignored element instead of `true` (default).
|
||||
* @returns {Function}
|
||||
*/
|
||||
CKEDITOR.dom.walker.ignored = function( isReject ) {
|
||||
return function( node ) {
|
||||
var isIgnored = isWhitespaces( node ) || isBookmark( node ) || isTemp( node );
|
||||
|
||||
return !!( isReject ^ isIgnored );
|
||||
};
|
||||
};
|
||||
|
||||
var isIgnored = CKEDITOR.dom.walker.ignored();
|
||||
|
||||
/**
|
||||
* Returns a function which checks whether the node is empty.
|
||||
*
|
||||
* @since 4.5
|
||||
* @static
|
||||
* @param {Boolean} [isReject=false] Whether to return `false` for the
|
||||
* ignored element instead of `true` (default).
|
||||
* @returns {Function}
|
||||
*/
|
||||
CKEDITOR.dom.walker.empty = function( isReject ) {
|
||||
return function( node ) {
|
||||
var i = 0,
|
||||
l = node.getChildCount();
|
||||
|
||||
for ( ; i < l; ++i ) {
|
||||
if ( !isIgnored( node.getChild( i ) ) ) {
|
||||
return !!isReject;
|
||||
}
|
||||
}
|
||||
|
||||
return !isReject;
|
||||
};
|
||||
};
|
||||
|
||||
var isEmpty = CKEDITOR.dom.walker.empty();
|
||||
|
||||
function filterTextContainers( dtd ) {
|
||||
var hash = {},
|
||||
name;
|
||||
|
||||
for ( name in dtd ) {
|
||||
if ( CKEDITOR.dtd[ name ][ '#' ] )
|
||||
hash[ name ] = 1;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* A hash of element names which in browsers that {@link CKEDITOR.env#needsBrFiller do not need `<br>` fillers}
|
||||
* can be selection containers despite being empty.
|
||||
*
|
||||
* @since 4.5
|
||||
* @static
|
||||
* @property {Object} validEmptyBlockContainers
|
||||
*/
|
||||
var validEmptyBlocks = CKEDITOR.dom.walker.validEmptyBlockContainers = CKEDITOR.tools.extend(
|
||||
filterTextContainers( CKEDITOR.dtd.$block ),
|
||||
{ caption: 1, td: 1, th: 1 }
|
||||
);
|
||||
|
||||
function isEditable( node ) {
|
||||
// Skip temporary elements, bookmarks and whitespaces.
|
||||
if ( isIgnored( node ) )
|
||||
return false;
|
||||
|
||||
if ( node.type == CKEDITOR.NODE_TEXT )
|
||||
return true;
|
||||
|
||||
if ( node.type == CKEDITOR.NODE_ELEMENT ) {
|
||||
// All inline and non-editable elements are valid editable places.
|
||||
// Note: the <hr> is currently the only element in CKEDITOR.dtd.$empty and CKEDITOR.dtd.$block,
|
||||
// but generally speaking we need an intersection of these two sets.
|
||||
// Note: non-editable block has to be treated differently (should be selected entirely).
|
||||
if ( node.is( CKEDITOR.dtd.$inline ) || node.is( 'hr' ) || node.getAttribute( 'contenteditable' ) == 'false' )
|
||||
return true;
|
||||
|
||||
// Empty blocks are editable on IE.
|
||||
if ( !CKEDITOR.env.needsBrFiller && node.is( validEmptyBlocks ) && isEmpty( node ) )
|
||||
return true;
|
||||
}
|
||||
|
||||
// Skip all other nodes.
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a function which checks whether the node can be a container or a sibling
|
||||
* of the selection end.
|
||||
*
|
||||
* This includes:
|
||||
*
|
||||
* * text nodes (but not whitespaces),
|
||||
* * inline elements,
|
||||
* * intersection of {@link CKEDITOR.dtd#$empty} and {@link CKEDITOR.dtd#$block} (currently
|
||||
* it is only `<hr>`),
|
||||
* * non-editable blocks (special case — such blocks cannot be containers nor
|
||||
* siblings, they need to be selected entirely),
|
||||
* * empty {@link #validEmptyBlockContainers blocks} which can contain text
|
||||
* ({@link CKEDITOR.env#needsBrFiller old IEs only}).
|
||||
*
|
||||
* @since 4.3
|
||||
* @static
|
||||
* @param {Boolean} [isReject=false] Whether to return `false` for the
|
||||
* ignored element instead of `true` (default).
|
||||
* @returns {Function}
|
||||
*/
|
||||
CKEDITOR.dom.walker.editable = function( isReject ) {
|
||||
return function( node ) {
|
||||
return !!( isReject ^ isEditable( node ) );
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if there is a filler node at the end of an element, and returns it.
|
||||
*
|
||||
* @member CKEDITOR.dom.element
|
||||
* @returns {CKEDITOR.dom.node/Boolean} Bogus node or `false`.
|
||||
*/
|
||||
CKEDITOR.dom.element.prototype.getBogus = function() {
|
||||
// Bogus are not always at the end, e.g. <p><a>text<br /></a></p> (#7070).
|
||||
var tail = this;
|
||||
do {
|
||||
tail = tail.getPreviousSourceNode();
|
||||
}
|
||||
while ( toSkip( tail ) );
|
||||
|
||||
if ( tail && ( CKEDITOR.env.needsBrFiller ? tail.is && tail.is( 'br' ) : tail.getText && tailNbspRegex.test( tail.getText() ) ) )
|
||||
return tail;
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
} )();
|
||||
95
ckeditor/core/dom/window.js
Normal file
95
ckeditor/core/dom/window.js
Normal file
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
|
||||
* For licensing, see LICENSE.md or http://ckeditor.com/license
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileOverview Defines the {@link CKEDITOR.dom.document} class, which
|
||||
* represents a DOM document.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents a DOM window.
|
||||
*
|
||||
* var document = new CKEDITOR.dom.window( window );
|
||||
*
|
||||
* @class
|
||||
* @extends CKEDITOR.dom.domObject
|
||||
* @constructor Creates a window class instance.
|
||||
* @param {Object} domWindow A native DOM window.
|
||||
*/
|
||||
CKEDITOR.dom.window = function( domWindow ) {
|
||||
CKEDITOR.dom.domObject.call( this, domWindow );
|
||||
};
|
||||
|
||||
CKEDITOR.dom.window.prototype = new CKEDITOR.dom.domObject();
|
||||
|
||||
CKEDITOR.tools.extend( CKEDITOR.dom.window.prototype, {
|
||||
/**
|
||||
* Moves the selection focus to this window.
|
||||
*
|
||||
* var win = new CKEDITOR.dom.window( window );
|
||||
* win.focus();
|
||||
*/
|
||||
focus: function() {
|
||||
this.$.focus();
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the width and height of this window's viewable area.
|
||||
*
|
||||
* var win = new CKEDITOR.dom.window( window );
|
||||
* var size = win.getViewPaneSize();
|
||||
* alert( size.width );
|
||||
* alert( size.height );
|
||||
*
|
||||
* @returns {Object} An object with the `width` and `height`
|
||||
* properties containing the size.
|
||||
*/
|
||||
getViewPaneSize: function() {
|
||||
var doc = this.$.document,
|
||||
stdMode = doc.compatMode == 'CSS1Compat';
|
||||
return {
|
||||
width: ( stdMode ? doc.documentElement.clientWidth : doc.body.clientWidth ) || 0,
|
||||
height: ( stdMode ? doc.documentElement.clientHeight : doc.body.clientHeight ) || 0
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the current position of the window's scroll.
|
||||
*
|
||||
* var win = new CKEDITOR.dom.window( window );
|
||||
* var pos = win.getScrollPosition();
|
||||
* alert( pos.x );
|
||||
* alert( pos.y );
|
||||
*
|
||||
* @returns {Object} An object with the `x` and `y` properties
|
||||
* containing the scroll position.
|
||||
*/
|
||||
getScrollPosition: function() {
|
||||
var $ = this.$;
|
||||
|
||||
if ( 'pageXOffset' in $ ) {
|
||||
return {
|
||||
x: $.pageXOffset || 0,
|
||||
y: $.pageYOffset || 0
|
||||
};
|
||||
} else {
|
||||
var doc = $.document;
|
||||
return {
|
||||
x: doc.documentElement.scrollLeft || doc.body.scrollLeft || 0,
|
||||
y: doc.documentElement.scrollTop || doc.body.scrollTop || 0
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the frame element containing this window context.
|
||||
*
|
||||
* @returns {CKEDITOR.dom.element} The frame element or `null` if not in a frame context.
|
||||
*/
|
||||
getFrame: function() {
|
||||
var iframe = this.$.frameElement;
|
||||
return iframe ? new CKEDITOR.dom.element.get( iframe ) : null;
|
||||
}
|
||||
} );
|
||||
Reference in New Issue
Block a user