ckeditor 4.3.4

This commit is contained in:
Roland Gruber 2014-04-11 20:07:18 +00:00
parent b59508b051
commit ace6f7d7d8
82 changed files with 32547 additions and 0 deletions

View File

@ -0,0 +1,153 @@
/**
* @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
( function() {
/** @class CKEDITOR */
/**
* Turns a DOM element with `contenteditable` attribute set to `true` into a
* CKEditor instance. Check {@link CKEDITOR.dtd#$editable} for the list of
* allowed element names.
*
* <div contenteditable="true" id="content">...</div>
* ...
* CKEDITOR.inline( 'content' );
*
* It is also possible to create an inline editor from the `<textarea>` element.
* If you do so, an additional `<div>` element with editable content will be created
* directly after the `<textarea>` element and the `<textarea>` element will be hidden.
*
* @param {Object/String} element The DOM element or its ID.
* @param {Object} [instanceConfig] The specific configurations to apply to this editor instance.
* See {@link CKEDITOR.config}.
* @returns {CKEDITOR.editor} The editor instance created.
*/
CKEDITOR.inline = function( element, instanceConfig ) {
if ( !CKEDITOR.env.isCompatible )
return null;
element = CKEDITOR.dom.element.get( element );
// Avoid multiple inline editor instances on the same element.
if ( element.getEditor() )
throw 'The editor instance "' + element.getEditor().name + '" is already attached to the provided element.';
var editor = new CKEDITOR.editor( instanceConfig, element, CKEDITOR.ELEMENT_MODE_INLINE ),
textarea = element.is( 'textarea' ) ? element : null;
if ( textarea ) {
editor.setData( textarea.getValue(), null, true );
//Change element from textarea to div
element = CKEDITOR.dom.element.createFromHtml(
'<div contenteditable="' + !!editor.readOnly + '" class="cke_textarea_inline">' +
textarea.getValue() +
'</div>',
CKEDITOR.document );
element.insertAfter( textarea );
textarea.hide();
// Attaching the concrete form.
if ( textarea.$.form )
editor._attachToForm();
} else {
// Initial editor data is simply loaded from the page element content to make
// data retrieval possible immediately after the editor creation.
editor.setData( element.getHtml(), null, true );
}
// Once the editor is loaded, start the UI.
editor.on( 'loaded', function() {
editor.fire( 'uiReady' );
// Enable editing on the element.
editor.editable( element );
// Editable itself is the outermost element.
editor.container = element;
// Load and process editor data.
editor.setData( editor.getData( 1 ) );
// Clean on startup.
editor.resetDirty();
editor.fire( 'contentDom' );
// Inline editing defaults to "wysiwyg" mode, so plugins don't
// need to make special handling for this "mode-less" environment.
editor.mode = 'wysiwyg';
editor.fire( 'mode' );
// The editor is completely loaded for interaction.
editor.status = 'ready';
editor.fireOnce( 'instanceReady' );
CKEDITOR.fire( 'instanceReady', null, editor );
// give priority to plugins that relay on editor#loaded for bootstrapping.
}, null, null, 10000 );
// Handle editor destroying.
editor.on( 'destroy', function() {
// Remove container from DOM if inline-textarea editor.
// Show <textarea> back again.
if ( textarea ) {
editor.container.clearCustomData();
editor.container.remove();
textarea.show();
}
editor.element.clearCustomData();
delete editor.element;
} );
return editor;
};
/**
* Calls {@link CKEDITOR#inline} for all page elements with
* `contenteditable` attribute set to `true`.
*
*/
CKEDITOR.inlineAll = function() {
var el, data;
for ( var name in CKEDITOR.dtd.$editable ) {
var elements = CKEDITOR.document.getElementsByTag( name );
for ( var i = 0, len = elements.count(); i < len; i++ ) {
el = elements.getItem( i );
if ( el.getAttribute( 'contenteditable' ) == 'true' ) {
// Fire the "inline" event, making it possible to customize
// the instance settings and eventually cancel the creation.
data = {
element: el,
config: {}
};
if ( CKEDITOR.fire( 'inline', data ) !== false )
CKEDITOR.inline( el, data.config );
}
}
}
};
CKEDITOR.domReady( function() {
!CKEDITOR.disableAutoInline && CKEDITOR.inlineAll();
} );
} )();
/**
* Disables creating the inline editor automatically for elements with
* `contenteditable` attribute set to the `true`.
*
* CKEDITOR.disableAutoInline = true;
*
* @cfg {Boolean} [disableAutoInline=false]
*/

View File

@ -0,0 +1,457 @@
/**
* @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
/** @class CKEDITOR */
/**
* The class name used to identify `<textarea>` elements to be replaced
* by CKEditor instances. Set it to empty/`null` to disable this feature.
*
* CKEDITOR.replaceClass = 'rich_editor';
*
* @cfg {String} [replaceClass='ckeditor']
*/
CKEDITOR.replaceClass = 'ckeditor';
( function() {
/**
* Replaces a `<textarea>` or a DOM element (`<div>`) with a CKEditor
* instance. For textareas, the initial value in the editor will be the
* textarea value. For DOM elements, their `innerHTML` will be used
* instead. We recommend using `<textarea>` and `<div>` elements only.
*
* <textarea id="myfield" name="myfield"></textarea>
* ...
* CKEDITOR.replace( 'myfield' );
*
* var textarea = document.body.appendChild( document.createElement( 'textarea' ) );
* CKEDITOR.replace( textarea );
*
* @param {Object/String} element The DOM element (textarea), its ID, or name.
* @param {Object} [config] The specific configuration to apply to this
* editor instance. Configuration set here will override the global CKEditor settings
* (see {@link CKEDITOR.config}).
* @returns {CKEDITOR.editor} The editor instance created.
*/
CKEDITOR.replace = function( element, config ) {
return createInstance( element, config, null, CKEDITOR.ELEMENT_MODE_REPLACE );
};
/**
* Creates a new editor instance at the end of a specific DOM element.
*
* <div id="editorSpace"></div>
* ...
* CKEDITOR.appendTo( 'editorSpace' );
*
* @param {Object/String} element The DOM element, its ID, or name.
* @param {Object} [config] The specific configuration to apply to this
* editor instance. Configuration set here will override the global CKEditor settings
* (see {@link CKEDITOR.config}).
* @param {String} [data] Since 3.3. Initial value for the instance.
* @returns {CKEDITOR.editor} The editor instance created.
*/
CKEDITOR.appendTo = function( element, config, data )
{
return createInstance( element, config, data, CKEDITOR.ELEMENT_MODE_APPENDTO );
};
/**
* Replaces all `<textarea>` elements available in the document with
* editor instances.
*
* // Replace all <textarea> elements in the page.
* CKEDITOR.replaceAll();
*
* // Replace all <textarea class="myClassName"> elements in the page.
* CKEDITOR.replaceAll( 'myClassName' );
*
* // Selectively replace <textarea> elements, based on custom assertions.
* CKEDITOR.replaceAll( function( textarea, config ) {
* // An assertion function that needs to be evaluated for the <textarea>
* // to be replaced. It must explicitely return "false" to ignore a
* // specific <textarea>.
* // You can also customize the editor instance by having the function
* // modify the "config" parameter.
* } );
*
* @param {String} [className] The `<textarea>` class name.
* @param {Function} [function] An assertion function that must return `true` for a `<textarea>`
* to be replaced with the editor. If the function returns `false`, the `<textarea>` element
* will not be replaced.
*/
CKEDITOR.replaceAll = function() {
var textareas = document.getElementsByTagName( 'textarea' );
for ( var i = 0; i < textareas.length; i++ ) {
var config = null,
textarea = textareas[ i ];
// The "name" and/or "id" attribute must exist.
if ( !textarea.name && !textarea.id )
continue;
if ( typeof arguments[ 0 ] == 'string' ) {
// The textarea class name could be passed as the function
// parameter.
var classRegex = new RegExp( '(?:^|\\s)' + arguments[ 0 ] + '(?:$|\\s)' );
if ( !classRegex.test( textarea.className ) )
continue;
} else if ( typeof arguments[ 0 ] == 'function' ) {
// An assertion function could be passed as the function parameter.
// It must explicitly return "false" to ignore a specific <textarea>.
config = {};
if ( arguments[ 0 ]( textarea, config ) === false )
continue;
}
this.replace( textarea, config );
}
};
/** @class CKEDITOR.editor */
/**
* Registers an editing mode. This function is to be used mainly by plugins.
*
* @param {String} mode The mode name.
* @param {Function} exec The function that performs the actual mode change.
*/
CKEDITOR.editor.prototype.addMode = function( mode, exec ) {
( this._.modes || ( this._.modes = {} ) )[ mode ] = exec;
};
/**
* Changes the editing mode of this editor instance.
*
* **Note:** The mode switch could be asynchronous depending on the mode provider.
* Use the `callback` to hook subsequent code.
*
* // Switch to "source" view.
* CKEDITOR.instances.editor1.setMode( 'source' );
* // Switch to "wysiwyg" view and be notified on completion.
* CKEDITOR.instances.editor1.setMode( 'wysiwyg', function() { alert( 'wysiwyg mode loaded!' ); } );
*
* @param {String} [newMode] If not specified, the {@link CKEDITOR.config#startupMode} will be used.
* @param {Function} [callback] Optional callback function which is invoked once the mode switch has succeeded.
*/
CKEDITOR.editor.prototype.setMode = function( newMode, callback ) {
var editor = this;
var modes = this._.modes;
// Mode loading quickly fails.
if ( newMode == editor.mode || !modes || !modes[ newMode ] )
return;
editor.fire( 'beforeSetMode', newMode );
if ( editor.mode ) {
var isDirty = editor.checkDirty();
editor._.previousMode = editor.mode;
editor.fire( 'beforeModeUnload' );
// Detach the current editable.
editor.editable( 0 );
// Clear up the mode space.
editor.ui.space( 'contents' ).setHtml( '' );
editor.mode = '';
}
// Fire the mode handler.
this._.modes[ newMode ]( function() {
// Set the current mode.
editor.mode = newMode;
if ( isDirty !== undefined )
!isDirty && editor.resetDirty();
// Delay to avoid race conditions (setMode inside setMode).
setTimeout( function() {
editor.fire( 'mode' );
callback && callback.call( editor );
}, 0 );
} );
};
/**
* Resizes the editor interface.
*
* editor.resize( 900, 300 );
*
* editor.resize( '100%', 450, true );
*
* @param {Number/String} width The new width. It can be an integer denoting a value
* in pixels or a CSS size value with unit.
* @param {Number/String} height The new height. It can be an integer denoting a value
* in pixels or a CSS size value with unit.
* @param {Boolean} [isContentHeight] Indicates that the provided height is to
* be applied to the editor content area, and not to the entire editor
* interface. Defaults to `false`.
* @param {Boolean} [resizeInner] Indicates that it is the inner interface
* element that must be resized, not the outer element. The default theme
* defines the editor interface inside a pair of `<span>` elements
* (`<span><span>...</span></span>`). By default the first,
* outer `<span>` element receives the sizes. If this parameter is set to
* `true`, the second, inner `<span>` is resized instead.
*/
CKEDITOR.editor.prototype.resize = function( width, height, isContentHeight, resizeInner ) {
var container = this.container,
contents = this.ui.space( 'contents' ),
contentsFrame = CKEDITOR.env.webkit && this.document && this.document.getWindow().$.frameElement,
outer = resizeInner ? container.getChild( 1 ) : container;
// Set as border box width. (#5353)
outer.setSize( 'width', width, true );
// WebKit needs to refresh the iframe size to avoid rendering issues. (1/2) (#8348)
contentsFrame && ( contentsFrame.style.width = '1%' );
// Get the height delta between the outer table and the content area.
// If we're setting the content area's height, then we don't need the delta.
var delta = isContentHeight ? 0 : ( outer.$.offsetHeight || 0 ) - ( contents.$.clientHeight || 0 );
contents.setStyle( 'height', Math.max( height - delta, 0 ) + 'px' );
// WebKit needs to refresh the iframe size to avoid rendering issues. (2/2) (#8348)
contentsFrame && ( contentsFrame.style.width = '100%' );
// Emit a resize event.
this.fire( 'resize' );
};
/**
* Gets the element that can be used to check the editor size. This method
* is mainly used by the `resize` plugin, which adds a UI handle that can be used
* to resize the editor.
*
* @param {Boolean} forContents Whether to return the "contents" part of the theme instead of the container.
* @returns {CKEDITOR.dom.element} The resizable element.
*/
CKEDITOR.editor.prototype.getResizable = function( forContents ) {
return forContents ? this.ui.space( 'contents' ) : this.container;
};
function createInstance( element, config, data, mode ) {
if ( !CKEDITOR.env.isCompatible )
return null;
element = CKEDITOR.dom.element.get( element );
// Avoid multiple inline editor instances on the same element.
if ( element.getEditor() )
throw 'The editor instance "' + element.getEditor().name + '" is already attached to the provided element.';
// Create the editor instance.
var editor = new CKEDITOR.editor( config, element, mode );
if ( mode == CKEDITOR.ELEMENT_MODE_REPLACE ) {
// Do not replace the textarea right now, just hide it. The effective
// replacement will be done later in the editor creation lifecycle.
element.setStyle( 'visibility', 'hidden' );
// #8031 Remember if textarea was required and remove the attribute.
editor._.required = element.hasAttribute( 'required' );
element.removeAttribute( 'required' );
}
data && editor.setData( data, null, true );
// Once the editor is loaded, start the UI.
editor.on( 'loaded', function() {
loadTheme( editor );
if ( mode == CKEDITOR.ELEMENT_MODE_REPLACE && editor.config.autoUpdateElement && element.$.form )
editor._attachToForm();
editor.setMode( editor.config.startupMode, function() {
// Clean on startup.
editor.resetDirty();
// Editor is completely loaded for interaction.
editor.status = 'ready';
editor.fireOnce( 'instanceReady' );
CKEDITOR.fire( 'instanceReady', null, editor );
} );
} );
editor.on( 'destroy', destroy );
return editor;
}
function destroy() {
var editor = this,
container = editor.container,
element = editor.element;
if ( container ) {
container.clearCustomData();
container.remove();
}
if ( element ) {
element.clearCustomData();
if ( editor.elementMode == CKEDITOR.ELEMENT_MODE_REPLACE ) {
element.show();
if ( editor._.required )
element.setAttribute( 'required', 'required' );
}
delete editor.element;
}
}
var themedTpl;
function loadTheme( editor ) {
var name = editor.name,
element = editor.element,
elementMode = editor.elementMode;
// Get the HTML for the predefined spaces.
var topHtml = editor.fire( 'uiSpace', { space: 'top', html: '' } ).html;
var bottomHtml = editor.fire( 'uiSpace', { space: 'bottom', html: '' } ).html;
if ( !themedTpl ) {
themedTpl = CKEDITOR.addTemplate( 'maincontainer', '<{outerEl}' +
' id="cke_{name}"' +
' class="{id} cke cke_reset cke_chrome cke_editor_{name} cke_{langDir} ' + CKEDITOR.env.cssClass + '" ' +
' dir="{langDir}"' +
' lang="{langCode}"' +
' role="application"' +
' aria-labelledby="cke_{name}_arialbl">' +
'<span id="cke_{name}_arialbl" class="cke_voice_label">{voiceLabel}</span>' +
'<{outerEl} class="cke_inner cke_reset" role="presentation">' +
'{topHtml}' +
'<{outerEl} id="{contentId}" class="cke_contents cke_reset" role="presentation"></{outerEl}>' +
'{bottomHtml}' +
'</{outerEl}>' +
'</{outerEl}>' );
}
var container = CKEDITOR.dom.element.createFromHtml( themedTpl.output( {
id: editor.id,
name: name,
langDir: editor.lang.dir,
langCode: editor.langCode,
voiceLabel: [ editor.lang.editor, editor.name ].join( ', ' ),
topHtml: topHtml ? '<span id="' + editor.ui.spaceId( 'top' ) + '" class="cke_top cke_reset_all" role="presentation" style="height:auto">' + topHtml + '</span>' : '',
contentId: editor.ui.spaceId( 'contents' ),
bottomHtml: bottomHtml ? '<span id="' + editor.ui.spaceId( 'bottom' ) + '" class="cke_bottom cke_reset_all" role="presentation">' + bottomHtml + '</span>' : '',
outerEl: CKEDITOR.env.ie ? 'span' : 'div' // #9571
} ) );
if ( elementMode == CKEDITOR.ELEMENT_MODE_REPLACE ) {
element.hide();
container.insertAfter( element );
} else
element.append( container );
editor.container = container;
// Make top and bottom spaces unelectable, but not content space,
// otherwise the editable area would be affected.
topHtml && editor.ui.space( 'top' ).unselectable();
bottomHtml && editor.ui.space( 'bottom' ).unselectable();
var width = editor.config.width, height = editor.config.height;
if ( width )
container.setStyle( 'width', CKEDITOR.tools.cssLength( width ) );
// The editor height is applied to the contents space.
if ( height )
editor.ui.space( 'contents' ).setStyle( 'height', CKEDITOR.tools.cssLength( height ) );
// Disable browser context menu for editor's chrome.
container.disableContextMenu();
// Redirect the focus into editor for webkit. (#5713)
CKEDITOR.env.webkit && container.on( 'focus', function() {
editor.focus();
} );
editor.fireOnce( 'uiReady' );
}
// Replace all textareas with the default class name.
CKEDITOR.domReady( function() {
CKEDITOR.replaceClass && CKEDITOR.replaceAll( CKEDITOR.replaceClass );
} );
} )();
/**
* The current editing mode. An editing mode basically provides
* different ways of editing or viewing the contents.
*
* alert( CKEDITOR.instances.editor1.mode ); // (e.g.) 'wysiwyg'
*
* @readonly
* @property {String} mode
*/
/**
* The mode to load at the editor startup. It depends on the plugins
* loaded. By default, the `wysiwyg` and `source` modes are available.
*
* config.startupMode = 'source';
*
* @cfg {String} [startupMode='wysiwyg']
* @member CKEDITOR.config
*/
CKEDITOR.config.startupMode = 'wysiwyg';
/**
* Fired after the editor instance is resized through
* the {@link CKEDITOR.editor#method-resize CKEDITOR.resize} method.
*
* @event resize
* @param {CKEDITOR.editor} editor This editor instance.
*/
/**
* Fired before changing the editing mode. See also
* {@link #beforeSetMode} and {@link #event-mode}.
*
* @event beforeModeUnload
* @param {CKEDITOR.editor} editor This editor instance.
*/
/**
* Fired before the editor mode is set. See also
* {@link #event-mode} and {@link #beforeModeUnload}.
*
* @since 3.5.3
* @event beforeSetMode
* @param {CKEDITOR.editor} editor This editor instance.
* @param {String} data The name of the mode which is about to be set.
*/
/**
* Fired after setting the editing mode. See also {@link #beforeSetMode} and {@link #beforeModeUnload}
*
* @event mode
* @param {CKEDITOR.editor} editor This editor instance.
*/
/**
* Fired when the editor (replacing a `<textarea>` which has a `required` attribute) is empty during form submission.
*
* This event replaces native required fields validation that the browsers cannot
* perform when CKEditor replaces `<textarea>` elements.
*
* You can cancel this event to prevent the page from submitting data.
*
* editor.on( 'required', function( evt ) {
* alert( 'Article content is required.' );
* evt.cancel();
* } );
*
* @event required
* @param {CKEDITOR.editor} editor This editor instance.
*/

View File

@ -0,0 +1,53 @@
/**
* @license Copyright (c) 2003-2014, 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 + '-->';
}
} );

View File

@ -0,0 +1,316 @@
/**
* @license Copyright (c) 2003-2014, 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 style sheet and inserts it into the document.
*
* @param cssStyleText {String} CSS style text.
* @returns {Object} The created DOM native style sheet 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 {@link CKEDITOR.dom.element} instance in this document.
*
* @returns {CKEDITOR.dom.element}
* @todo
*/
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 {@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}
*/
getActive: function() {
return new CKEDITOR.dom.element( this.$.activeElement );
},
/**
* 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 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 holds this document.
*
* @returns {CKEDITOR.dom.window} The window object.
*/
getWindow: function() {
return new CKEDITOR.dom.window( this.$.parentWindow || this.$.defaultView );
},
/**
* Defines the document contents through document.write. Note that the
* previous document contents will be lost (cleaned).
*
* document.write(
* '<html>' +
* '<head><title>Sample Doc</title></head>' +
* '<body>Document contents created by code</body>' +
* '</html>'
* );
*
* @since 3.5
* @param {String} html The HTML defining the document contents.
*/
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
* specified `selector`.
*
* **Note:** returned list is not a live collection (like a 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 first element within this document that matches
* 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;
},
/**
* IE8 only method. It returns 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;
}
} );

View File

@ -0,0 +1,45 @@
/**
* @license Copyright (c) 2003-2014, 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 DocumentFragment objects
* as arguments and results in all the child nodes of the DocumentFragment being
* moved to the child list of this node.
*
* @class
* @constructor Creates a document fragment class instance.
* @param {Object} nodeOrDoc
* @todo example and param doc
*/
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 document fragment's contents after specified node.
*
* @param {CKEDITOR.dom.node} node
*/
insertAfterNode: function( node ) {
node = node.$;
node.parentNode.insertBefore( this.$, node.nextSibling );
}
}, true, { 'append': 1, 'appendBogus': 1, 'getFirst': 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 } );

View File

@ -0,0 +1,262 @@
/**
* @license Copyright (c) 2003-2014, 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 {
/**
* Get 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 guarantied to be available on this unique dom node,
* thus any wish to continue access 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.
*
* 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 data slot under 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 on 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 identiquely identify this DOM object in
* the running session.
*
* @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 );

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,251 @@
/**
* @license Copyright (c) 2003-2014, 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 );
}
};

View File

@ -0,0 +1,208 @@
/**
* @license Copyright (c) 2003-2014, 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;

View File

@ -0,0 +1,500 @@
/**
* @license Copyright (c) 2003-2014, 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 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 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 iterator will return two list item elements.
* // With enforceRealBlocks set to true 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;
this.range = range;
this.forceBrBreak = 0;
// (#3730).
/**
* Whether include `<br>`s into the enlarged range. Should be
* set to `false` when using iterator in {@link CKEDITOR#ENTER_BR} mode.
*
* @property {Boolean} [enlargeBr=true]
*/
this.enlargeBr = 1;
/**
* Whether iterator should create transformable block
* if the current one contains text and it 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 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 );
};
// Get a reference for the next element, bookmark nodes are skipped.
function getNextSourceNode( node, startFromSibling, lastNode ) {
var next = node.getNextSourceNode( startFromSibling, null, lastNode );
while ( !bookmarkGuard( next ) )
next = next.getNextSourceNode( startFromSibling, null, lastNode );
return next;
}
iterator.prototype = {
/**
* Returns next paragraph-like element or `null` if reached the end of range.
*
* @param {String} [blockTag='p'] Name of a block element which will be established by
* 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 &nbsp; 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 = 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.getName() == 'li' ) ) {
// 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 : 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 : getNextSourceNode( block, 1, lastNode );
return block;
}
};
// @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;
// Shrink the range to exclude harmful "noises" (#4087, #4450, #5435).
range.shrink( CKEDITOR.SHRINK_ELEMENT, true );
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 );
// 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 == undefined )
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;
}
/**
* Creates {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 );
};
} )();

View File

@ -0,0 +1,748 @@
/**
* @license Copyright (c) 2003-2014, 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;
CKEDITOR.POSITION_IDENTICAL = 0;
CKEDITOR.POSITION_DISCONNECTED = 1;
CKEDITOR.POSITION_FOLLOWING = 2;
CKEDITOR.POSITION_PRECEDING = 4;
CKEDITOR.POSITION_IS_CONTAINED = 8;
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;
},
/**
* Clone this node.
*
* **Note**: Values set by {#setCustomData} won't 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 );
var removeIds = function( 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 )
return;
if ( !cloneId )
node.removeAttribute( 'id', false );
if ( includeChildren ) {
var childs = node.childNodes;
for ( var i = 0; i < childs.length; i++ )
removeIds( childs[ i ] );
}
};
// The "id" attribute should never be cloned to avoid duplication.
removeIds( $clone );
return new CKEDITOR.dom.node( $clone );
},
/**
* Check if node is preceded by any sibling.
*
* @returns {Boolean}
*/
hasPrevious: function() {
return !!this.$.previousSibling;
},
/**
* Check if 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 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 );
},
/**
* Get index of a node in an array of its parent.childNodes.
*
* Let's assume having childNodes array:
*
* [ emptyText, element1, text, text, element2 ]
* element1.getIndex(); // 1
* element1.getIndex( true ); // 0
* element2.getIndex(); // 4
* element2.getIndex( true ); // 2
*
* @param {Boolean} normalized When `true` empty text nodes and one followed
* by another one text node are not counted in.
* @returns {Number} Index of a node.
*/
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 index;
do {
// Bypass blank node and adjacent text nodes.
if ( normalized && current != this.$ && current.nodeType == CKEDITOR.NODE_TEXT && ( isNormalizing || !current.nodeValue ) )
continue;
index++;
isNormalizing = current.nodeType == CKEDITOR.NODE_TEXT;
}
while ( ( current = current.previousSibling ) )
return index;
},
/**
* @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 preceed 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 array containing node parents and node itself. By default nodes are in _descending_ order.
*
* // Assuming that body has paragraph as 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 order of returned nodes.
* @returns {Array} Returns 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;
},
/**
* @todo
*/
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 preceed/follow relationship.
for ( var i = 0; i <= minLevel - 1; i++ ) {
if ( addressOfThis[ i ] != addressOfOther[ i ] ) {
if ( i < minLevel )
return addressOfThis[ i ] < addressOfOther[ i ] ? CKEDITOR.POSITION_PRECEDING : CKEDITOR.POSITION_FOLLOWING;
break;
}
}
// 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.
*
* // 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">
*
* @since 3.6.1
* @param {String} reference The name of the ancestor node to search or
* an object with the node names to search for.
* @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( reference, includeSelf ) {
var $ = this.$,
name;
if ( !includeSelf )
$ = $.parentNode;
while ( $ ) {
if ( $.nodeName && ( name = $.nodeName.toLowerCase(), ( typeof reference == 'string' ? name == reference : name in reference ) ) )
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).
*
* **Note:** When `attributeCheck` is not used, this method only work for elements
* that are already presented in the document, otherwise the result
* is not guaranteed, it's mainly for performance consideration.
*
* // For the following HTML:
* // <div contenteditable="false">Some <b>text</b></div>
*
* // If "ele" is the above <div>
* element.isReadOnly(); // true
*
* @since 3.5
* @returns {Boolean}
*/
isReadOnly: function() {
var element = this;
if ( this.type != CKEDITOR.NODE_ELEMENT )
element = this.getParent();
if ( 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' ) )
break;
if ( element.getAttribute( 'contentEditable' ) == 'false' )
return true;
else if ( element.getAttribute( 'contentEditable' ) == 'true' )
break;
element = element.getParent();
}
// Reached the root of DOM tree, no editable found.
return !element;
}
}
} );

View File

@ -0,0 +1,43 @@
/**
* @license Copyright (c) 2003-2014, 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;
}
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,201 @@
/**
* @license Copyright (c) 2003-2014, 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(),
guard = function( node ) {
return !( node.is && node.is( 'tr' ) );
},
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
*/

View File

@ -0,0 +1,139 @@
/**
* @license Copyright (c) 2003-2014, 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 = CKEDITOR.dom.text( nativeNode );
*
* var text = 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 );
}
} );

View File

@ -0,0 +1,616 @@
/**
* @license Copyright (c) 2003-2014, 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 a 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 totaly included into the
* range boundaires. Let's 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 guard function is instead called both when
* entering and leaving nodes.
*
* @class
*/
CKEDITOR.dom.walker = CKEDITOR.tools.createClass( {
/**
* Creates a walker class instance.
*
* @constructor
* @param {CKEDITOR.dom.range} range The range within which walk.
*/
$: function( range ) {
this.range = range;
/**
* A function executed for every matched node, to check whether
* it's to be considered into 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 pass by to check
* whether the walk is to be finished. It's called when both
* entering and 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 wich 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
// * &lt;body&gt; 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 gets called.
*/
end: function() {
this._.end = 1;
},
/**
* Retrieves the next node (at 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 (at left).
*
* @returns {CKEDITOR.dom.node} The previous node or null if no more
* nodes are available.
*/
previous: function() {
return iterate.call( this, 1 );
},
/**
* Check all nodes at 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 at 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 at 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 at the left or null
* if no valid nodes are available.
*/
lastBackward: function() {
return iterateToLast.call( this, 1 );
},
/**
* Resets 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 element is displayed as a block.
*
* @member CKEDITOR.dom.element
* @param [customNodeNames] Custom list of nodes which will extend
* 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, type ) {
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 bookmark node
* inner contents.
*
* @static
* @param {Boolean} [contentOnly=false] Whether only test against the text content of
* bookmark node instead of the element itself (default).
* @param {Boolean} [isReject=false] Whether should 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 whitespaces 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 text cursor filler node we used in Webkit. (#9384)
isWhitespace = !CKEDITOR.tools.trim( node.getText() ) ||
CKEDITOR.env.webkit && node.getText() == '\u200b';
}
return !!( isReject ^ isWhitespace );
};
};
/**
* Returns a function which checks whether the node is invisible in wysiwyg mode.
*
* @static
* @param {Boolean} [isReject=false]
* @returns {Function}
*/
CKEDITOR.dom.walker.invisible = function( isReject ) {
var whitespace = CKEDITOR.dom.walker.whitespaces();
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> we're checking here
// 'offsetHeight' instead of 'offsetWidth' for properly excluding
// all sorts of empty paragraph, e.g. <br />.
invisible = !node.$.offsetHeight;
}
return !!( isReject ^ invisible );
};
};
/**
* Returns a function which checks whether node's type is equal to 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 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 node is a temporary element
* (element with `data-cke-temp` attribute) or its child.
*
* @since 4.3
* @static
* @param {Boolean} [isReject=false] Whether should 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 ]*(?:&nbsp;|\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 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 should 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();
function isEmpty( node ) {
var i = 0,
l = node.getChildCount();
for ( ; i < l; ++i ) {
if ( !isIgnored( node.getChild( i ) ) )
return false;
}
return true;
}
function filterTextContainers( dtd ) {
var hash = {},
name;
for ( name in dtd ) {
if ( CKEDITOR.dtd[ name ][ '#' ] )
hash[ name ] = 1;
}
return hash;
}
// Block elements which can contain text nodes (without ul, ol, dl, etc.).
var dtdTextBlock = filterTextContainers( CKEDITOR.dtd.$block );
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: non-editable block has to be treated differently (should be selected entirely).
if ( node.is( CKEDITOR.dtd.$inline ) || node.getAttribute( 'contenteditable' ) == 'false' )
return true;
// Empty blocks are editable on IE.
if ( !CKEDITOR.env.needsBrFiller && node.is( dtdTextBlock ) && isEmpty( node ) )
return true;
}
// Skip all other nodes.
return false;
}
/**
* Returns a function which checks whether node can be a container or a sibling
* of selection end.
*
* This includes:
*
* * text nodes (but not whitespaces),
* * inline elements,
* * non-editable blocks (special case - such blocks cannot be containers nor
* siblings, they need to be selected entirely),
* * empty blocks which can contain text (IE only).
*
* @since 4.3
* @static
* @param {Boolean} [isReject=false] Whether should 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's 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;
};
} )();

View File

@ -0,0 +1,95 @@
/**
* @license Copyright (c) 2003-2014, 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;
}
} );

View File

@ -0,0 +1,152 @@
/**
* @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
/**
* TODO
*
* @class
* @todo
*/
CKEDITOR.htmlParser.basicWriter = CKEDITOR.tools.createClass( {
/**
* Creates a basicWriter class instance.
*
* @constructor
*/
$: function() {
this._ = {
output: []
};
},
proto: {
/**
* Writes the tag opening part for a opener tag.
*
* // Writes '<p'.
* writer.openTag( 'p', { class : 'MyClass', id : 'MyId' } );
*
* @param {String} tagName The element name for this tag.
* @param {Object} attributes The attributes defined for this tag. The
* attributes could be used to inspect the tag.
*/
openTag: function( tagName, attributes ) {
this._.output.push( '<', tagName );
},
/**
* Writes the tag closing part for a opener tag.
*
* // Writes '>'.
* writer.openTagClose( 'p', false );
*
* // Writes ' />'.
* writer.openTagClose( 'br', true );
*
* @param {String} tagName The element name for this tag.
* @param {Boolean} isSelfClose Indicates that this is a self-closing tag,
* like `<br>` or `<img>`.
*/
openTagClose: function( tagName, isSelfClose ) {
if ( isSelfClose )
this._.output.push( ' />' );
else
this._.output.push( '>' );
},
/**
* Writes an attribute. This function should be called after opening the
* tag with {@link #openTagClose}.
*
* // Writes ' class="MyClass"'.
* writer.attribute( 'class', 'MyClass' );
*
* @param {String} attName The attribute name.
* @param {String} attValue The attribute value.
*/
attribute: function( attName, attValue ) {
// Browsers don't always escape special character in attribute values. (#4683, #4719).
if ( typeof attValue == 'string' )
attValue = CKEDITOR.tools.htmlEncodeAttr( attValue );
this._.output.push( ' ', attName, '="', attValue, '"' );
},
/**
* Writes a closer tag.
*
* // Writes '</p>'.
* writer.closeTag( 'p' );
*
* @param {String} tagName The element name for this tag.
*/
closeTag: function( tagName ) {
this._.output.push( '</', tagName, '>' );
},
/**
* Writes text.
*
* // Writes 'Hello Word'.
* writer.text( 'Hello Word' );
*
* @param {String} text The text value.
*/
text: function( text ) {
this._.output.push( text );
},
/**
* Writes a comment.
*
* // Writes '<!-- My comment -->'.
* writer.comment( ' My comment ' );
*
* @param {String} comment The comment text.
*/
comment: function( comment ) {
this._.output.push( '<!--', comment, '-->' );
},
/**
* Writes any kind of data to the ouput.
*
* writer.write( 'This is an <b>example</b>.' );
*
* @param {String} data
*/
write: function( data ) {
this._.output.push( data );
},
/**
* Empties the current output buffer.
*
* writer.reset();
*/
reset: function() {
this._.output = [];
this._.indent = false;
},
/**
* Empties the current output buffer.
*
* var html = writer.getHtml();
*
* @param {Boolean} reset Indicates that the {@link #reset} method is to
* be automatically called after retrieving the HTML.
* @returns {String} The HTML written to the writer so far.
*/
getHtml: function( reset ) {
var html = this._.output.join( '' );
if ( reset )
this.reset();
return html;
}
}
} );

View File

@ -0,0 +1,48 @@
/**
* @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
'use strict';
( function() {
/**
* A lightweight representation of HTML CDATA.
*
* @class
* @extends CKEDITOR.htmlParser.node
* @constructor Creates a cdata class instance.
* @param {String} value The CDATA section value.
*/
CKEDITOR.htmlParser.cdata = function( value ) {
/**
* The CDATA value.
*
* @property {String}
*/
this.value = value;
};
CKEDITOR.htmlParser.cdata.prototype = CKEDITOR.tools.extend( new CKEDITOR.htmlParser.node(), {
/**
* CDATA has the same type as {@link CKEDITOR.htmlParser.text} This is
* a constant value set to {@link CKEDITOR#NODE_TEXT}.
*
* @readonly
* @property {Number} [=CKEDITOR.NODE_TEXT]
*/
type: CKEDITOR.NODE_TEXT,
filter: function() {},
/**
* Writes the CDATA with no special manipulations.
*
* @param {CKEDITOR.htmlParser.basicWriter} writer The writer to which write the HTML.
*/
writeHtml: function( writer ) {
writer.write( this.value );
}
} );
} )();

View File

@ -0,0 +1,80 @@
/**
* @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
'use strict';
/**
* A lightweight representation of an HTML comment.
*
* @class
* @extends CKEDITOR.htmlParser.node
* @constructor Creates a comment class instance.
* @param {String} value The comment text value.
*/
CKEDITOR.htmlParser.comment = function( value ) {
/**
* The comment text.
*
* @property {String}
*/
this.value = value;
/** @private */
this._ = {
isBlockLike: false
};
};
CKEDITOR.htmlParser.comment.prototype = CKEDITOR.tools.extend( new CKEDITOR.htmlParser.node(), {
/**
* The node type. This is a constant value set to {@link CKEDITOR#NODE_COMMENT}.
*
* @readonly
* @property {Number} [=CKEDITOR.NODE_COMMENT]
*/
type: CKEDITOR.NODE_COMMENT,
/**
* Filter this comment with given filter.
*
* @since 4.1
* @param {CKEDITOR.htmlParser.filter} filter
* @returns {Boolean} Method returns `false` when this comment has
* been removed or replaced with other node. This is an information for
* {@link CKEDITOR.htmlParser.element#filterChildren} that it has
* to repeat filter on current position in parent's children array.
*/
filter: function( filter, context ) {
var comment = this.value;
if ( !( comment = filter.onComment( context, comment, this ) ) ) {
this.remove();
return false;
}
if ( typeof comment != 'string' ) {
this.replaceWith( comment );
return false;
}
this.value = comment;
return true;
},
/**
* Writes the HTML representation of this comment to a CKEDITOR.htmlWriter.
*
* @param {CKEDITOR.htmlParser.basicWriter} writer The writer to which write the HTML.
* @param {CKEDITOR.htmlParser.filter} [filter] The filter to be applied to this node.
* **Note:** it's unsafe to filter offline (not appended) node.
*/
writeHtml: function( writer, filter ) {
if ( filter )
this.filter( filter );
writer.comment( this.value );
}
} );

View File

@ -0,0 +1,519 @@
/**
* @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
'use strict';
/**
* A lightweight representation of an HTML element.
*
* @class
* @extends CKEDITOR.htmlParser.node
* @constructor Creates an element class instance.
* @param {String} name The element name.
* @param {Object} attributes And object holding all attributes defined for
* this element.
*/
CKEDITOR.htmlParser.element = function( name, attributes ) {
/**
* The element name.
*
* @property {String}
*/
this.name = name;
/**
* Holds the attributes defined for this element.
*
* @property {Object}
*/
this.attributes = attributes || {};
/**
* The nodes that are direct children of this element.
*/
this.children = [];
// Reveal the real semantic of our internal custom tag name (#6639),
// when resolving whether it's block like.
var realName = name || '',
prefixed = realName.match( /^cke:(.*)/ );
prefixed && ( realName = prefixed[ 1 ] );
var isBlockLike = !!( CKEDITOR.dtd.$nonBodyContent[ realName ] || CKEDITOR.dtd.$block[ realName ] || CKEDITOR.dtd.$listItem[ realName ] || CKEDITOR.dtd.$tableContent[ realName ] || CKEDITOR.dtd.$nonEditable[ realName ] || realName == 'br' );
this.isEmpty = !!CKEDITOR.dtd.$empty[ name ];
this.isUnknown = !CKEDITOR.dtd[ name ];
/** @private */
this._ = {
isBlockLike: isBlockLike,
hasInlineStarted: this.isEmpty || !isBlockLike
};
};
/**
* Object presentation of CSS style declaration text.
*
* @class
* @constructor Creates a cssStyle class instance.
* @param {CKEDITOR.htmlParser.element/String} elementOrStyleText
* A html parser element or the inline style text.
*/
CKEDITOR.htmlParser.cssStyle = function() {
var styleText,
arg = arguments[ 0 ],
rules = {};
styleText = arg instanceof CKEDITOR.htmlParser.element ? arg.attributes.style : arg;
// html-encoded quote might be introduced by 'font-family'
// from MS-Word which confused the following regexp. e.g.
//'font-family: &quot;Lucida, Console&quot;'
// TODO reuse CSS methods from tools.
( styleText || '' ).replace( /&quot;/g, '"' ).replace( /\s*([^ :;]+)\s*:\s*([^;]+)\s*(?=;|$)/g, function( match, name, value ) {
name == 'font-family' && ( value = value.replace( /["']/g, '' ) );
rules[ name.toLowerCase() ] = value;
} );
return {
rules: rules,
/**
* Apply the styles onto the specified element or object.
*
* @param {CKEDITOR.htmlParser.element/CKEDITOR.dom.element/Object} obj
*/
populate: function( obj ) {
var style = this.toString();
if ( style )
obj instanceof CKEDITOR.dom.element ? obj.setAttribute( 'style', style ) : obj instanceof CKEDITOR.htmlParser.element ? obj.attributes.style = style : obj.style = style;
},
/**
* Serialize CSS style declaration to string.
*
* @returns {String}
*/
toString: function() {
var output = [];
for ( var i in rules )
rules[ i ] && output.push( i, ':', rules[ i ], ';' );
return output.join( '' );
}
};
};
/** @class CKEDITOR.htmlParser.element */
( function() {
// Used to sort attribute entries in an array, where the first element of
// each object is the attribute name.
var sortAttribs = function( a, b ) {
a = a[ 0 ];
b = b[ 0 ];
return a < b ? -1 : a > b ? 1 : 0;
},
fragProto = CKEDITOR.htmlParser.fragment.prototype;
CKEDITOR.htmlParser.element.prototype = CKEDITOR.tools.extend( new CKEDITOR.htmlParser.node(), {
/**
* The node type. This is a constant value set to {@link CKEDITOR#NODE_ELEMENT}.
*
* @readonly
* @property {Number} [=CKEDITOR.NODE_ELEMENT]
*/
type: CKEDITOR.NODE_ELEMENT,
/**
* Adds a node to the element children list.
*
* @method
* @param {CKEDITOR.htmlParser.node} node The node to be added.
* @param {Number} [index] From where the insertion happens.
*/
add: fragProto.add,
/**
* Clone this element.
*
* @returns {CKEDITOR.htmlParser.element} The element clone.
*/
clone: function() {
return new CKEDITOR.htmlParser.element( this.name, this.attributes );
},
/**
* Filter this element and its children with given filter.
*
* @since 4.1
* @param {CKEDITOR.htmlParser.filter} filter
* @returns {Boolean} Method returns `false` when this element has
* been removed or replaced with other. This is an information for
* {@link #filterChildren} that it has to repeat filter on current
* position in parent's children array.
*/
filter: function( filter, context ) {
var element = this,
originalName, name;
context = element.getFilterContext( context );
// Do not process elements with data-cke-processor attribute set to off.
if ( context.off )
return true;
// Filtering if it's the root node.
if ( !element.parent )
filter.onRoot( context, element );
while ( true ) {
originalName = element.name;
if ( !( name = filter.onElementName( context, originalName ) ) ) {
this.remove();
return false;
}
element.name = name;
if ( !( element = filter.onElement( context, element ) ) ) {
this.remove();
return false;
}
// New element has been returned - replace current one
// and process it (stop processing this and return false, what
// means that element has been removed).
if ( element !== this ) {
this.replaceWith( element );
return false;
}
// If name has been changed - continue loop, so in next iteration
// filters for new name will be applied to this element.
// If name hasn't been changed - stop.
if ( element.name == originalName )
break;
// If element has been replaced with something of a
// different type, then make the replacement filter itself.
if ( element.type != CKEDITOR.NODE_ELEMENT ) {
this.replaceWith( element );
return false;
}
// This indicate that the element has been dropped by
// filter but not the children.
if ( !element.name ) {
this.replaceWithChildren();
return false;
}
}
var attributes = element.attributes,
a, value, newAttrName;
for ( a in attributes ) {
newAttrName = a;
value = attributes[ a ];
// Loop until name isn't modified.
// A little bit senseless, but IE would do that anyway
// because it iterates with for-in loop even over properties
// created during its run.
while ( true ) {
if ( !( newAttrName = filter.onAttributeName( context, a ) ) ) {
delete attributes[ a ];
break;
} else if ( newAttrName != a ) {
delete attributes[ a ];
a = newAttrName;
continue;
} else
break;
}
if ( newAttrName ) {
if ( ( value = filter.onAttribute( context, element, newAttrName, value ) ) === false )
delete attributes[ newAttrName ];
else
attributes[ newAttrName ] = value;
}
}
if ( !element.isEmpty )
this.filterChildren( filter, false, context );
return true;
},
/**
* Filter this element's children with given filter.
*
* Element's children may only be filtered once by one
* instance of filter.
*
* @method filterChildren
* @param {CKEDITOR.htmlParser.filter} filter
*/
filterChildren: fragProto.filterChildren,
/**
* Writes the element HTML to a CKEDITOR.htmlWriter.
*
* @param {CKEDITOR.htmlParser.basicWriter} writer The writer to which write the HTML.
* @param {CKEDITOR.htmlParser.filter} [filter] The filter to be applied to this node.
* **Note:** it's unsafe to filter offline (not appended) node.
*/
writeHtml: function( writer, filter ) {
if ( filter )
this.filter( filter );
var name = this.name,
attribsArray = [],
attributes = this.attributes,
attrName,
attr, i, l;
// Open element tag.
writer.openTag( name, attributes );
// Copy all attributes to an array.
for ( attrName in attributes )
attribsArray.push( [ attrName, attributes[ attrName ] ] );
// Sort the attributes by name.
if ( writer.sortAttributes )
attribsArray.sort( sortAttribs );
// Send the attributes.
for ( i = 0, l = attribsArray.length; i < l; i++ ) {
attr = attribsArray[ i ];
writer.attribute( attr[ 0 ], attr[ 1 ] );
}
// Close the tag.
writer.openTagClose( name, this.isEmpty );
this.writeChildrenHtml( writer );
// Close the element.
if ( !this.isEmpty )
writer.closeTag( name );
},
/**
* Send children of this element to the writer.
*
* @param {CKEDITOR.htmlParser.basicWriter} writer The writer to which write the HTML.
* @param {CKEDITOR.htmlParser.filter} [filter]
*/
writeChildrenHtml: fragProto.writeChildrenHtml,
/**
* Replace this element with its children.
*
* @since 4.1
*/
replaceWithChildren: function() {
var children = this.children;
for ( var i = children.length; i; )
children[ --i ].insertAfter( this );
this.remove();
},
/**
* Execute callback on each node (of given type) in this element.
*
* // Create <p> element with foo<b>bar</b>bom as its content.
* var elP = CKEDITOR.htmlParser.fragment.fromHtml( 'foo<b>bar</b>bom', 'p' );
* elP.forEach( function( node ) {
* console.log( node );
* } );
* // Will log:
* // 1. document fragment,
* // 2. <p> element,
* // 3. "foo" text node,
* // 4. <b> element,
* // 5. "bar" text node,
* // 6. "bom" text node.
*
* @since 4.1
* @param {Function} callback Function to be executed on every node.
* **Since 4.3** if `callback` returned `false` descendants of current node will be ignored.
* @param {CKEDITOR.htmlParser.node} callback.node Node passed as argument.
* @param {Number} [type] If specified `callback` will be executed only on nodes of this type.
* @param {Boolean} [skipRoot] Don't execute `callback` on this element.
*/
forEach: fragProto.forEach,
/**
* Gets this element's first child. If `condition` is given returns
* first child which satisfies that condition.
*
* @since 4.3
* @param {String/Object/Function} condition Name of a child, hash of names or validator function.
* @returns {CKEDITOR.htmlParser.node}
*/
getFirst: function( condition ) {
if ( !condition )
return this.children.length ? this.children[ 0 ] : null;
if ( typeof condition != 'function' )
condition = nameCondition( condition );
for ( var i = 0, l = this.children.length; i < l; ++i ) {
if ( condition( this.children[ i ] ) )
return this.children[ i ];
}
return null;
},
/**
* Gets this element's inner HTML.
*
* @since 4.3
* @returns {String}
*/
getHtml: function() {
var writer = new CKEDITOR.htmlParser.basicWriter();
this.writeChildrenHtml( writer );
return writer.getHtml();
},
/**
* Sets this element's inner HTML.
*
* @since 4.3
* @param {String} html
*/
setHtml: function( html ) {
var children = this.children = CKEDITOR.htmlParser.fragment.fromHtml( html ).children;
for ( var i = 0, l = children.length; i < l; ++i )
children[ i ].parent = this;
},
/**
* Gets this element's outer HTML.
*
* @since 4.3
* @returns {String}
*/
getOuterHtml: function() {
var writer = new CKEDITOR.htmlParser.basicWriter();
this.writeHtml( writer );
return writer.getHtml();
},
/**
* Splits this element at given index.
*
* @since 4.3
* @param {Number} index Index at which element will be split &ndash; `0` means beginning,
* `1` after first child node, etc.
* @returns {CKEDITOR.htmlParser.element} New element, following this one.
*/
split: function( index ) {
var cloneChildren = this.children.splice( index, this.children.length - index ),
clone = this.clone();
for ( var i = 0; i < cloneChildren.length; ++i )
cloneChildren[ i ].parent = clone;
clone.children = cloneChildren;
if ( cloneChildren[ 0 ] )
cloneChildren[ 0 ].previous = null;
if ( index > 0 )
this.children[ index - 1 ].next = null;
this.parent.add( clone, this.getIndex() + 1 );
return clone;
},
/**
* Removes class name from classes list.
*
* @since 4.3
* @param {String} className The class name to be removed.
*/
removeClass: function( className ) {
var classes = this.attributes[ 'class' ],
index;
if ( !classes )
return;
// We can safely assume that className won't break regexp.
// http://stackoverflow.com/questions/448981/what-characters-are-valid-in-css-class-names
classes = CKEDITOR.tools.trim( classes.replace( new RegExp( '(?:\\s+|^)' + className + '(?:\\s+|$)' ), ' ' ) );
if ( classes )
this.attributes[ 'class' ] = classes;
else
delete this.attributes[ 'class' ];
},
/**
* Checkes whether this element has a class name.
*
* @since 4.3
* @param {String} className The class name to be checked.
* @returns {Boolean} Whether this element has a `className`.
*/
hasClass: function( className ) {
var classes = this.attributes[ 'class' ];
if ( !classes )
return false;
return ( new RegExp( '(?:^|\\s)' + className + '(?=\\s|$)' ) ).test( classes );
},
getFilterContext: function( ctx ) {
var changes = [];
if ( !ctx ) {
ctx = {
off: false,
nonEditable: false,
nestedEditable: false
};
}
if ( !ctx.off && this.attributes[ 'data-cke-processor' ] == 'off' )
changes.push( 'off', true );
if ( !ctx.nonEditable && this.attributes.contenteditable == 'false' )
changes.push( 'nonEditable', true );
// A context to be given nestedEditable must be nonEditable first (by inheritance) (#11372).
// Never set "nestedEditable" context for a body. If body is processed then it indicates
// a fullPage editor and there's no slightest change of nesting such editable (#11504).
else if ( this.name != 'body' && !ctx.nestedEditable && this.attributes.contenteditable == 'true' )
changes.push( 'nestedEditable', true );
if ( changes.length ) {
ctx = CKEDITOR.tools.copy( ctx );
for ( var i = 0; i < changes.length; i += 2 )
ctx[ changes[ i ] ] = changes[ i + 1 ];
}
return ctx;
}
}, true );
function nameCondition( condition ) {
return function( el ) {
return el.type == CKEDITOR.NODE_ELEMENT &&
( typeof condition == 'string' ? el.name == condition : el.name in condition );
};
}
} )();

View File

@ -0,0 +1,407 @@
/**
* @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
'use strict';
( function() {
/**
* Filter is a configurable tool for transforming and filtering {@link CKEDITOR.htmlParser.node nodes}.
* It is mainly used during data processing phase which is done not on real DOM nodes,
* but on their simplified form represented by {@link CKEDITOR.htmlParser.node} class and its subclasses.
*
* var filter = new CKEDITOR.htmlParser.filter( {
* text: function( value ) {
* return '@' + value + '@';
* },
* elements: {
* p: function( element ) {
* element.attributes.foo = '1';
* }
* }
* } );
*
* var fragment = CKEDITOR.htmlParser.fragment.fromHtml( '<p>Foo<b>bar!</b></p>' ),
* writer = new CKEDITOR.htmlParser.basicWriter();
* filter.applyTo( fragment );
* fragment.writeHtml( writer );
* writer.getHtml(); // '<p foo="1">@Foo@<b>@bar!@</b></p>'
*
* @class
*/
CKEDITOR.htmlParser.filter = CKEDITOR.tools.createClass( {
/**
* @constructor Creates a filter class instance.
* @param {CKEDITOR.htmlParser.filterRulesDefinition} [rules]
*/
$: function( rules ) {
/**
* ID of filter instance, which is used to mark elements
* to which this filter has been already applied.
*
* @property {Number} id
* @readonly
*/
this.id = CKEDITOR.tools.getNextNumber();
/**
* Rules for element names.
*
* @property {CKEDITOR.htmlParser.filterRulesGroup}
* @readonly
*/
this.elementNameRules = new filterRulesGroup();
/**
* Rules for attribute names.
*
* @property {CKEDITOR.htmlParser.filterRulesGroup}
* @readonly
*/
this.attributeNameRules = new filterRulesGroup();
/**
* Hash of elementName => {@link CKEDITOR.htmlParser.filterRulesGroup rules for elements}.
*
* @readonly
*/
this.elementsRules = {};
/**
* Hash of attributeName => {@link CKEDITOR.htmlParser.filterRulesGroup rules for attributes}.
*
* @readonly
*/
this.attributesRules = {};
/**
* Rules for text nodes.
*
* @property {CKEDITOR.htmlParser.filterRulesGroup}
* @readonly
*/
this.textRules = new filterRulesGroup();
/**
* Rules for comment nodes.
*
* @property {CKEDITOR.htmlParser.filterRulesGroup}
* @readonly
*/
this.commentRules = new filterRulesGroup();
/**
* Rules for a root node.
*
* @property {CKEDITOR.htmlParser.filterRulesGroup}
* @readonly
*/
this.rootRules = new filterRulesGroup();
if ( rules )
this.addRules( rules, 10 );
},
proto: {
/**
* Add rules to this filter.
*
* @param {CKEDITOR.htmlParser.filterRulesDefinition} rules Object containing filter rules.
* @param {Object/Number} [options] Object containing rules' options or a priority
* (for a backward compatibility with CKEditor versions up to 4.2.x).
* @param {Number} [options.priority=10] The priority of a rule.
* @param {Boolean} [options.applyToAll=false] Whether to apply rule to non-editable
* elements and their descendants too.
*/
addRules: function( rules, options ) {
var priority;
// Backward compatibility.
if ( typeof options == 'number' )
priority = options;
// New version - try reading from options.
else if ( options && ( 'priority' in options ) )
priority = options.priority;
// Defaults.
if ( typeof priority != 'number' )
priority = 10;
if ( typeof options != 'object' )
options = {};
// Add the elementNames.
if ( rules.elementNames )
this.elementNameRules.addMany( rules.elementNames, priority, options );
// Add the attributeNames.
if ( rules.attributeNames )
this.attributeNameRules.addMany( rules.attributeNames, priority, options );
// Add the elements.
if ( rules.elements )
addNamedRules( this.elementsRules, rules.elements, priority, options );
// Add the attributes.
if ( rules.attributes )
addNamedRules( this.attributesRules, rules.attributes, priority, options );
// Add the text.
if ( rules.text )
this.textRules.add( rules.text, priority, options );
// Add the comment.
if ( rules.comment )
this.commentRules.add( rules.comment, priority, options );
// Add root node rules.
if ( rules.root )
this.rootRules.add( rules.root, priority, options );
},
/**
* Apply this filter to given node.
*
* @param {CKEDITOR.htmlParser.node} node The node to be filtered.
*/
applyTo: function( node ) {
node.filter( this );
},
onElementName: function( context, name ) {
return this.elementNameRules.execOnName( context, name );
},
onAttributeName: function( context, name ) {
return this.attributeNameRules.execOnName( context, name );
},
onText: function( context, text, node ) {
return this.textRules.exec( context, text, node );
},
onComment: function( context, commentText, comment ) {
return this.commentRules.exec( context, commentText, comment );
},
onRoot: function( context, element ) {
return this.rootRules.exec( context, element );
},
onElement: function( context, element ) {
// We must apply filters set to the specific element name as
// well as those set to the generic ^/$ name. So, add both to an
// array and process them in a small loop.
var rulesGroups = [ this.elementsRules[ '^' ], this.elementsRules[ element.name ], this.elementsRules.$ ],
rulesGroup, ret;
for ( var i = 0; i < 3; i++ ) {
rulesGroup = rulesGroups[ i ];
if ( rulesGroup ) {
ret = rulesGroup.exec( context, element, this );
if ( ret === false )
return null;
if ( ret && ret != element )
return this.onNode( context, ret );
// The non-root element has been dismissed by one of the filters.
if ( element.parent && !element.name )
break;
}
}
return element;
},
onNode: function( context, node ) {
var type = node.type;
return type == CKEDITOR.NODE_ELEMENT ? this.onElement( context, node ) :
type == CKEDITOR.NODE_TEXT ? new CKEDITOR.htmlParser.text( this.onText( context, node.value ) ) :
type == CKEDITOR.NODE_COMMENT ? new CKEDITOR.htmlParser.comment( this.onComment( context, node.value ) ) : null;
},
onAttribute: function( context, element, name, value ) {
var rulesGroup = this.attributesRules[ name ];
if ( rulesGroup )
return rulesGroup.exec( context, value, element, this );
return value;
}
}
} );
/**
* Class grouping filter rules for one subject (like element or attribute names).
*
* @class CKEDITOR.htmlParser.filterRulesGroup
*/
function filterRulesGroup() {
/**
* Array of objects containing rule, priority and options.
*
* @property {Object[]}
* @readonly
*/
this.rules = [];
}
CKEDITOR.htmlParser.filterRulesGroup = filterRulesGroup;
filterRulesGroup.prototype = {
/**
* Adds specified rule to this group.
*
* @param {Function/Array} rule Function for function based rule or [ pattern, replacement ] array for
* rule applicable to names.
* @param {Number} priority
* @param options
*/
add: function( rule, priority, options ) {
this.rules.splice( this.findIndex( priority ), 0, {
value: rule,
priority: priority,
options: options
} );
},
/**
* Adds specified rules to this group.
*
* @param {Array} rules Array of rules - see {@link #add}.
* @param {Number} priority
* @param options
*/
addMany: function( rules, priority, options ) {
var args = [ this.findIndex( priority ), 0 ];
for ( var i = 0, len = rules.length; i < len; i++ ) {
args.push( {
value: rules[ i ],
priority: priority,
options: options
} );
}
this.rules.splice.apply( this.rules, args );
},
/**
* Finds an index at which rule with given priority should be inserted.
*
* @param {Number} priority
* @returns {Number} Index.
*/
findIndex: function( priority ) {
var rules = this.rules,
len = rules.length,
i = len - 1;
// Search from the end, because usually rules will be added with default priority, so
// we will be able to stop loop quickly.
while ( i >= 0 && priority < rules[ i ].priority )
i--;
return i + 1;
},
/**
* Executes this rules group on given value. Applicable only if function based rules were added.
*
* All arguments passed to this function will be forwarded to rules' functions.
*
* @param {CKEDITOR.htmlParser.node/CKEDITOR.htmlParser.fragment/String} currentValue The value to be filtered.
* @returns {CKEDITOR.htmlParser.node/CKEDITOR.htmlParser.fragment/String} Filtered value.
*/
exec: function( context, currentValue ) {
var isNode = currentValue instanceof CKEDITOR.htmlParser.node || currentValue instanceof CKEDITOR.htmlParser.fragment,
// Splice '1' to remove context, which we don't want to pass to filter rules.
args = Array.prototype.slice.call( arguments, 1 ),
rules = this.rules,
len = rules.length,
orgType, orgName, ret, i, rule;
for ( i = 0; i < len; i++ ) {
// Backup the node info before filtering.
if ( isNode ) {
orgType = currentValue.type;
orgName = currentValue.name;
}
rule = rules[ i ];
if ( isRuleApplicable( context, rule ) ) {
ret = rule.value.apply( null, args );
if ( ret === false )
return ret;
// We're filtering node (element/fragment).
// No further filtering if it's not anymore fitable for the subsequent filters.
if ( isNode && ret && ( ret.name != orgName || ret.type != orgType ) )
return ret;
// Update currentValue and corresponding argument in args array.
// Updated values will be used in next for-loop step.
if ( ret != undefined )
args[ 0 ] = currentValue = ret;
// ret == undefined will continue loop as nothing has happened.
}
}
return currentValue;
},
/**
* Executes this rules group on name. Applicable only if filter rules for names were added.
*
* @param {String} currentName The name to be filtered.
* @returns {String} Filtered name.
*/
execOnName: function( context, currentName ) {
var i = 0,
rules = this.rules,
len = rules.length,
rule;
for ( ; currentName && i < len; i++ ) {
rule = rules[ i ];
if ( isRuleApplicable( context, rule ) )
currentName = currentName.replace( rule.value[ 0 ], rule.value[ 1 ] );
}
return currentName;
}
};
function addNamedRules( rulesGroups, newRules, priority, options ) {
var ruleName, rulesGroup;
for ( ruleName in newRules ) {
rulesGroup = rulesGroups[ ruleName ];
if ( !rulesGroup )
rulesGroup = rulesGroups[ ruleName ] = new filterRulesGroup();
rulesGroup.add( newRules[ ruleName ], priority, options );
}
}
function isRuleApplicable( context, rule ) {
if ( context.nonEditable && !rule.options.applyToAll )
return false;
if ( context.nestedEditable && rule.options.excludeNestedEditable )
return false;
return true;
}
} )();
/**
* @class CKEDITOR.htmlParser.filterRulesDefinition
* @abstract
*/

View File

@ -0,0 +1,643 @@
/**
* @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
'use strict';
/**
* A lightweight representation of an HTML DOM structure.
*
* @class
* @constructor Creates a fragment class instance.
*/
CKEDITOR.htmlParser.fragment = function() {
/**
* The nodes contained in the root of this fragment.
*
* var fragment = CKEDITOR.htmlParser.fragment.fromHtml( '<b>Sample</b> Text' );
* alert( fragment.children.length ); // 2
*/
this.children = [];
/**
* Get the fragment parent. Should always be null.
*
* @property {Object} [=null]
*/
this.parent = null;
/** @private */
this._ = {
isBlockLike: true,
hasInlineStarted: false
};
};
( function() {
// Block-level elements whose internal structure should be respected during
// parser fixing.
var nonBreakingBlocks = CKEDITOR.tools.extend( { table: 1, ul: 1, ol: 1, dl: 1 }, CKEDITOR.dtd.table, CKEDITOR.dtd.ul, CKEDITOR.dtd.ol, CKEDITOR.dtd.dl );
var listBlocks = { ol: 1, ul: 1 };
// Dtd of the fragment element, basically it accept anything except for intermediate structure, e.g. orphan <li>.
var rootDtd = CKEDITOR.tools.extend( {}, { html: 1 }, CKEDITOR.dtd.html, CKEDITOR.dtd.body, CKEDITOR.dtd.head, { style: 1, script: 1 } );
// Which element to create when encountered not allowed content.
var structureFixes = {
ul: 'li',
ol: 'li',
dl: 'dd',
table: 'tbody',
tbody: 'tr',
thead: 'tr',
tfoot: 'tr',
tr: 'td'
};
function isRemoveEmpty( node ) {
// Keep marked element event if it is empty.
if ( node.attributes[ 'data-cke-survive' ] )
return false;
// Empty link is to be removed when empty but not anchor. (#7894)
return node.name == 'a' && node.attributes.href || CKEDITOR.dtd.$removeEmpty[ node.name ];
}
/**
* Creates a {@link CKEDITOR.htmlParser.fragment} from an HTML string.
*
* var fragment = CKEDITOR.htmlParser.fragment.fromHtml( '<b>Sample</b> Text' );
* alert( fragment.children[ 0 ].name ); // 'b'
* alert( fragment.children[ 1 ].value ); // ' Text'
*
* @static
* @param {String} fragmentHtml The HTML to be parsed, filling the fragment.
* @param {CKEDITOR.htmlParser.element/String} [parent] Optional contextual
* element which makes the content been parsed as the content of this element and fix
* to match it.
* If not provided, then {@link CKEDITOR.htmlParser.fragment} will be used
* as the parent and it will be returned.
* @param {String/Boolean} [fixingBlock] When `parent` is a block limit element,
* and the param is a string value other than `false`, it is to
* avoid having block-less content as the direct children of parent by wrapping
* the content with a block element of the specified tag, e.g.
* when `fixingBlock` specified as `p`, the content `<body><i>foo</i></body>`
* will be fixed into `<body><p><i>foo</i></p></body>`.
* @returns {CKEDITOR.htmlParser.fragment/CKEDITOR.htmlParser.element} The created fragment or passed `parent`.
*/
CKEDITOR.htmlParser.fragment.fromHtml = function( fragmentHtml, parent, fixingBlock ) {
var parser = new CKEDITOR.htmlParser();
var root = parent instanceof CKEDITOR.htmlParser.element ? parent : typeof parent == 'string' ? new CKEDITOR.htmlParser.element( parent ) : new CKEDITOR.htmlParser.fragment();
var pendingInline = [],
pendingBRs = [],
currentNode = root,
// Indicate we're inside a <textarea> element, spaces should be touched differently.
inTextarea = root.name == 'textarea',
// Indicate we're inside a <pre> element, spaces should be touched differently.
inPre = root.name == 'pre';
function checkPending( newTagName ) {
var pendingBRsSent;
if ( pendingInline.length > 0 ) {
for ( var i = 0; i < pendingInline.length; i++ ) {
var pendingElement = pendingInline[ i ],
pendingName = pendingElement.name,
pendingDtd = CKEDITOR.dtd[ pendingName ],
currentDtd = currentNode.name && CKEDITOR.dtd[ currentNode.name ];
if ( ( !currentDtd || currentDtd[ pendingName ] ) && ( !newTagName || !pendingDtd || pendingDtd[ newTagName ] || !CKEDITOR.dtd[ newTagName ] ) ) {
if ( !pendingBRsSent ) {
sendPendingBRs();
pendingBRsSent = 1;
}
// Get a clone for the pending element.
pendingElement = pendingElement.clone();
// Add it to the current node and make it the current,
// so the new element will be added inside of it.
pendingElement.parent = currentNode;
currentNode = pendingElement;
// Remove the pending element (back the index by one
// to properly process the next entry).
pendingInline.splice( i, 1 );
i--;
} else {
// Some element of the same type cannot be nested, flat them,
// e.g. <a href="#">foo<a href="#">bar</a></a>. (#7894)
if ( pendingName == currentNode.name )
addElement( currentNode, currentNode.parent, 1 ), i--;
}
}
}
}
function sendPendingBRs() {
while ( pendingBRs.length )
addElement( pendingBRs.shift(), currentNode );
}
// Rtrim empty spaces on block end boundary. (#3585)
function removeTailWhitespace( element ) {
if ( element._.isBlockLike && element.name != 'pre' && element.name != 'textarea' ) {
var length = element.children.length,
lastChild = element.children[ length - 1 ],
text;
if ( lastChild && lastChild.type == CKEDITOR.NODE_TEXT ) {
if ( !( text = CKEDITOR.tools.rtrim( lastChild.value ) ) )
element.children.length = length - 1;
else
lastChild.value = text;
}
}
}
// Beside of simply append specified element to target, this function also takes
// care of other dirty lifts like forcing block in body, trimming spaces at
// the block boundaries etc.
//
// @param {Element} element The element to be added as the last child of {@link target}.
// @param {Element} target The parent element to relieve the new node.
// @param {Boolean} [moveCurrent=false] Don't change the "currentNode" global unless
// there's a return point node specified on the element, otherwise move current onto {@link target} node.
//
function addElement( element, target, moveCurrent ) {
target = target || currentNode || root;
// Current element might be mangled by fix body below,
// save it for restore later.
var savedCurrent = currentNode;
// Ignore any element that has already been added.
if ( element.previous === undefined ) {
if ( checkAutoParagraphing( target, element ) ) {
// Create a <p> in the fragment.
currentNode = target;
parser.onTagOpen( fixingBlock, {} );
// The new target now is the <p>.
element.returnPoint = target = currentNode;
}
removeTailWhitespace( element );
// Avoid adding empty inline.
if ( !( isRemoveEmpty( element ) && !element.children.length ) )
target.add( element );
if ( element.name == 'pre' )
inPre = false;
if ( element.name == 'textarea' )
inTextarea = false;
}
if ( element.returnPoint ) {
currentNode = element.returnPoint;
delete element.returnPoint;
} else
currentNode = moveCurrent ? target : savedCurrent;
}
// Auto paragraphing should happen when inline content enters the root element.
function checkAutoParagraphing( parent, node ) {
// Check for parent that can contain block.
if ( ( parent == root || parent.name == 'body' ) && fixingBlock &&
( !parent.name || CKEDITOR.dtd[ parent.name ][ fixingBlock ] ) )
{
var name, realName;
if ( node.attributes && ( realName = node.attributes[ 'data-cke-real-element-type' ] ) )
name = realName;
else
name = node.name;
// Text node, inline elements are subjected, except for <script>/<style>.
return name && name in CKEDITOR.dtd.$inline &&
!( name in CKEDITOR.dtd.head ) &&
!node.isOrphan ||
node.type == CKEDITOR.NODE_TEXT;
}
}
// Judge whether two element tag names are likely the siblings from the same
// structural element.
function possiblySibling( tag1, tag2 ) {
if ( tag1 in CKEDITOR.dtd.$listItem || tag1 in CKEDITOR.dtd.$tableContent )
return tag1 == tag2 || tag1 == 'dt' && tag2 == 'dd' || tag1 == 'dd' && tag2 == 'dt';
return false;
}
parser.onTagOpen = function( tagName, attributes, selfClosing, optionalClose ) {
var element = new CKEDITOR.htmlParser.element( tagName, attributes );
// "isEmpty" will be always "false" for unknown elements, so we
// must force it if the parser has identified it as a selfClosing tag.
if ( element.isUnknown && selfClosing )
element.isEmpty = true;
// Check for optional closed elements, including browser quirks and manually opened blocks.
element.isOptionalClose = optionalClose;
// This is a tag to be removed if empty, so do not add it immediately.
if ( isRemoveEmpty( element ) ) {
pendingInline.push( element );
return;
} else if ( tagName == 'pre' )
inPre = true;
else if ( tagName == 'br' && inPre ) {
currentNode.add( new CKEDITOR.htmlParser.text( '\n' ) );
return;
} else if ( tagName == 'textarea' )
inTextarea = true;
if ( tagName == 'br' ) {
pendingBRs.push( element );
return;
}
while ( 1 ) {
var currentName = currentNode.name;
var currentDtd = currentName ? ( CKEDITOR.dtd[ currentName ] || ( currentNode._.isBlockLike ? CKEDITOR.dtd.div : CKEDITOR.dtd.span ) ) : rootDtd;
// If the element cannot be child of the current element.
if ( !element.isUnknown && !currentNode.isUnknown && !currentDtd[ tagName ] ) {
// Current node doesn't have a close tag, time for a close
// as this element isn't fit in. (#7497)
if ( currentNode.isOptionalClose )
parser.onTagClose( currentName );
// Fixing malformed nested lists by moving it into a previous list item. (#3828)
else if ( tagName in listBlocks && currentName in listBlocks ) {
var children = currentNode.children,
lastChild = children[ children.length - 1 ];
// Establish the list item if it's not existed.
if ( !( lastChild && lastChild.name == 'li' ) )
addElement( ( lastChild = new CKEDITOR.htmlParser.element( 'li' ) ), currentNode );
!element.returnPoint && ( element.returnPoint = currentNode );
currentNode = lastChild;
}
// Establish new list root for orphan list items, but NOT to create
// new list for the following ones, fix them instead. (#6975)
// <dl><dt>foo<dd>bar</dl>
// <ul><li>foo<li>bar</ul>
else if ( tagName in CKEDITOR.dtd.$listItem &&
!possiblySibling( tagName, currentName ) ) {
parser.onTagOpen( tagName == 'li' ? 'ul' : 'dl', {}, 0, 1 );
}
// We're inside a structural block like table and list, AND the incoming element
// is not of the same type (e.g. <td>td1<td>td2</td>), we simply add this new one before it,
// and most importantly, return back to here once this element is added,
// e.g. <table><tr><td>td1</td><p>p1</p><td>td2</td></tr></table>
else if ( currentName in nonBreakingBlocks &&
!possiblySibling( tagName, currentName ) ) {
!element.returnPoint && ( element.returnPoint = currentNode );
currentNode = currentNode.parent;
} else {
// The current element is an inline element, which
// need to be continued even after the close, so put
// it in the pending list.
if ( currentName in CKEDITOR.dtd.$inline )
pendingInline.unshift( currentNode );
// The most common case where we just need to close the
// current one and append the new one to the parent.
if ( currentNode.parent )
addElement( currentNode, currentNode.parent, 1 );
// We've tried our best to fix the embarrassment here, while
// this element still doesn't find it's parent, mark it as
// orphan and show our tolerance to it.
else {
element.isOrphan = 1;
break;
}
}
} else
break;
}
checkPending( tagName );
sendPendingBRs();
element.parent = currentNode;
if ( element.isEmpty )
addElement( element );
else
currentNode = element;
};
parser.onTagClose = function( tagName ) {
// Check if there is any pending tag to be closed.
for ( var i = pendingInline.length - 1; i >= 0; i-- ) {
// If found, just remove it from the list.
if ( tagName == pendingInline[ i ].name ) {
pendingInline.splice( i, 1 );
return;
}
}
var pendingAdd = [],
newPendingInline = [],
candidate = currentNode;
while ( candidate != root && candidate.name != tagName ) {
// If this is an inline element, add it to the pending list, if we're
// really closing one of the parents element later, they will continue
// after it.
if ( !candidate._.isBlockLike )
newPendingInline.unshift( candidate );
// This node should be added to it's parent at this point. But,
// it should happen only if the closing tag is really closing
// one of the nodes. So, for now, we just cache it.
pendingAdd.push( candidate );
// Make sure return point is properly restored.
candidate = candidate.returnPoint || candidate.parent;
}
if ( candidate != root ) {
// Add all elements that have been found in the above loop.
for ( i = 0; i < pendingAdd.length; i++ ) {
var node = pendingAdd[ i ];
addElement( node, node.parent );
}
currentNode = candidate;
if ( candidate._.isBlockLike )
sendPendingBRs();
addElement( candidate, candidate.parent );
// The parent should start receiving new nodes now, except if
// addElement changed the currentNode.
if ( candidate == currentNode )
currentNode = currentNode.parent;
pendingInline = pendingInline.concat( newPendingInline );
}
if ( tagName == 'body' )
fixingBlock = false;
};
parser.onText = function( text ) {
// Trim empty spaces at beginning of text contents except <pre> and <textarea>.
if ( ( !currentNode._.hasInlineStarted || pendingBRs.length ) && !inPre && !inTextarea ) {
text = CKEDITOR.tools.ltrim( text );
if ( text.length === 0 )
return;
}
var currentName = currentNode.name,
currentDtd = currentName ? ( CKEDITOR.dtd[ currentName ] || ( currentNode._.isBlockLike ? CKEDITOR.dtd.div : CKEDITOR.dtd.span ) ) : rootDtd;
// Fix orphan text in list/table. (#8540) (#8870)
if ( !inTextarea && !currentDtd[ '#' ] && currentName in nonBreakingBlocks ) {
parser.onTagOpen( structureFixes[ currentName ] || '' );
parser.onText( text );
return;
}
sendPendingBRs();
checkPending();
// Shrinking consequential spaces into one single for all elements
// text contents.
if ( !inPre && !inTextarea )
text = text.replace( /[\t\r\n ]{2,}|[\t\r\n]/g, ' ' );
text = new CKEDITOR.htmlParser.text( text );
if ( checkAutoParagraphing( currentNode, text ) )
this.onTagOpen( fixingBlock, {}, 0, 1 );
currentNode.add( text );
};
parser.onCDATA = function( cdata ) {
currentNode.add( new CKEDITOR.htmlParser.cdata( cdata ) );
};
parser.onComment = function( comment ) {
sendPendingBRs();
checkPending();
currentNode.add( new CKEDITOR.htmlParser.comment( comment ) );
};
// Parse it.
parser.parse( fragmentHtml );
sendPendingBRs();
// Close all pending nodes, make sure return point is properly restored.
while ( currentNode != root )
addElement( currentNode, currentNode.parent, 1 );
removeTailWhitespace( root );
return root;
};
CKEDITOR.htmlParser.fragment.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,
/**
* Adds a node to this fragment.
*
* @param {CKEDITOR.htmlParser.node} node The node to be added.
* @param {Number} [index] From where the insertion happens.
*/
add: function( node, index ) {
isNaN( index ) && ( index = this.children.length );
var previous = index > 0 ? this.children[ index - 1 ] : null;
if ( previous ) {
// If the block to be appended is following text, trim spaces at
// the right of it.
if ( node._.isBlockLike && previous.type == CKEDITOR.NODE_TEXT ) {
previous.value = CKEDITOR.tools.rtrim( previous.value );
// If we have completely cleared the previous node.
if ( previous.value.length === 0 ) {
// Remove it from the list and add the node again.
this.children.pop();
this.add( node );
return;
}
}
previous.next = node;
}
node.previous = previous;
node.parent = this;
this.children.splice( index, 0, node );
if ( !this._.hasInlineStarted )
this._.hasInlineStarted = node.type == CKEDITOR.NODE_TEXT || ( node.type == CKEDITOR.NODE_ELEMENT && !node._.isBlockLike );
},
/**
* Filter this fragment's content with given filter.
*
* @since 4.1
* @param {CKEDITOR.htmlParser.filter} filter
*/
filter: function( filter, context ) {
context = this.getFilterContext( context );
// Apply the root filter.
filter.onRoot( context, this );
this.filterChildren( filter, false, context );
},
/**
* Filter this fragment's children with given filter.
*
* Element's children may only be filtered once by one
* instance of filter.
*
* @since 4.1
* @param {CKEDITOR.htmlParser.filter} filter
* @param {Boolean} [filterRoot] Whether to apply the "root" filter rule specified in the `filter`.
*/
filterChildren: function( filter, filterRoot, context ) {
// If this element's children were already filtered
// by current filter, don't filter them 2nd time.
// This situation may occur when filtering bottom-up
// (filterChildren() called manually in element's filter),
// or in unpredictable edge cases when filter
// is manipulating DOM structure.
if ( this.childrenFilteredBy == filter.id )
return;
context = this.getFilterContext( context );
// Filtering root if enforced.
if ( filterRoot && !this.parent )
filter.onRoot( context, this );
this.childrenFilteredBy = filter.id;
// Don't cache anything, children array may be modified by filter rule.
for ( var i = 0; i < this.children.length; i++ ) {
// Stay in place if filter returned false, what means
// that node has been removed.
if ( this.children[ i ].filter( filter, context ) === false )
i--;
}
},
/**
* Writes the fragment HTML to a {@link CKEDITOR.htmlParser.basicWriter}.
*
* var writer = new CKEDITOR.htmlWriter();
* var fragment = CKEDITOR.htmlParser.fragment.fromHtml( '<P><B>Example' );
* fragment.writeHtml( writer );
* alert( writer.getHtml() ); // '<p><b>Example</b></p>'
*
* @param {CKEDITOR.htmlParser.basicWriter} writer The writer to which write the HTML.
* @param {CKEDITOR.htmlParser.filter} [filter] The filter to use when writing the HTML.
*/
writeHtml: function( writer, filter ) {
if ( filter )
this.filter( filter );
this.writeChildrenHtml( writer );
},
/**
* Write and filtering the child nodes of this fragment.
*
* @param {CKEDITOR.htmlParser.basicWriter} writer The writer to which write the HTML.
* @param {CKEDITOR.htmlParser.filter} [filter] The filter to use when writing the HTML.
* @param {Boolean} [filterRoot] Whether to apply the "root" filter rule specified in the `filter`.
*/
writeChildrenHtml: function( writer, filter, filterRoot ) {
var context = this.getFilterContext();
// Filtering root if enforced.
if ( filterRoot && !this.parent && filter )
filter.onRoot( context, this );
if ( filter )
this.filterChildren( filter, false, context );
for ( var i = 0, children = this.children, l = children.length; i < l; i++ )
children[ i ].writeHtml( writer );
},
/**
* Execute callback on each node (of given type) in this document fragment.
*
* var fragment = CKEDITOR.htmlParser.fragment.fromHtml( '<p>foo<b>bar</b>bom</p>' );
* fragment.forEach( function( node ) {
* console.log( node );
* } );
* // Will log:
* // 1. document fragment,
* // 2. <p> element,
* // 3. "foo" text node,
* // 4. <b> element,
* // 5. "bar" text node,
* // 6. "bom" text node.
*
* @since 4.1
* @param {Function} callback Function to be executed on every node.
* **Since 4.3** if `callback` returned `false` descendants of current node will be ignored.
* @param {CKEDITOR.htmlParser.node} callback.node Node passed as argument.
* @param {Number} [type] If specified `callback` will be executed only on nodes of this type.
* @param {Boolean} [skipRoot] Don't execute `callback` on this fragment.
*/
forEach: function( callback, type, skipRoot ) {
if ( !skipRoot && ( !type || this.type == type ) )
var ret = callback( this );
// Do not filter children if callback returned false.
if ( ret === false )
return;
var children = this.children,
node,
i = 0;
// We do not cache the size, because the list of nodes may be changed by the callback.
for ( ; i < children.length; i++ ) {
node = children[ i ];
if ( node.type == CKEDITOR.NODE_ELEMENT )
node.forEach( callback, type );
else if ( !type || node.type == type )
callback( node );
}
},
getFilterContext: function( context ) {
return context || {};
}
};
} )();

View File

@ -0,0 +1,150 @@
/**
* @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
'use strict';
( function() {
/**
* A lightweight representation of HTML node.
*
* @since 4.1
* @class
* @constructor Creates a node class instance.
*/
CKEDITOR.htmlParser.node = function() {};
CKEDITOR.htmlParser.node.prototype = {
/**
* Remove this node from a tree.
*
* @since 4.1
*/
remove: function() {
var children = this.parent.children,
index = CKEDITOR.tools.indexOf( children, this ),
previous = this.previous,
next = this.next;
previous && ( previous.next = next );
next && ( next.previous = previous );
children.splice( index, 1 );
this.parent = null;
},
/**
* Replace this node with given one.
*
* @since 4.1
* @param {CKEDITOR.htmlParser.node} node The node that will replace this one.
*/
replaceWith: function( node ) {
var children = this.parent.children,
index = CKEDITOR.tools.indexOf( children, this ),
previous = node.previous = this.previous,
next = node.next = this.next;
previous && ( previous.next = node );
next && ( next.previous = node );
children[ index ] = node;
node.parent = this.parent;
this.parent = null;
},
/**
* Insert this node after given one.
*
* @since 4.1
* @param {CKEDITOR.htmlParser.node} node The node that will precede this element.
*/
insertAfter: function( node ) {
var children = node.parent.children,
index = CKEDITOR.tools.indexOf( children, node ),
next = node.next;
children.splice( index + 1, 0, this );
this.next = node.next;
this.previous = node;
node.next = this;
next && ( next.previous = this );
this.parent = node.parent;
},
/**
* Insert this node before given one.
*
* @since 4.1
* @param {CKEDITOR.htmlParser.node} node The node that will follow this element.
*/
insertBefore: function( node ) {
var children = node.parent.children,
index = CKEDITOR.tools.indexOf( children, node );
children.splice( index, 0, this );
this.next = node;
this.previous = node.previous;
node.previous && ( node.previous.next = this );
node.previous = this;
this.parent = node.parent;
},
/**
* Gets the closest ancestor element of this element which satisfies given condition
*
* @since 4.3
* @param {String/Object/Function} condition Name of an ancestor, hash of names or validator function.
* @returns {CKEDITOR.htmlParser.element} The closest ancestor which satisfies given condition or `null`.
*/
getAscendant: function( condition ) {
var checkFn =
typeof condition == 'function' ? condition :
typeof condition == 'string' ? function( el ) { return el.name == condition; } :
function( el ) { return el.name in condition; };
var parent = this.parent;
// Parent has to be an element - don't check doc fragment.
while ( parent && parent.type == CKEDITOR.NODE_ELEMENT ) {
if ( checkFn( parent ) )
return parent;
parent = parent.parent;
}
return null;
},
/**
* Wraps this element with given `wrapper`.
*
* @since 4.3
* @param {CKEDITOR.htmlParser.element} wrapper The element which will be this element's new parent.
* @returns {CKEDITOR.htmlParser.element} Wrapper.
*/
wrapWith: function( wrapper ) {
this.replaceWith( wrapper );
wrapper.add( this );
return wrapper;
},
/**
* Gets this node's index in its parent's children array.
*
* @since 4.3
* @returns {Number}
*/
getIndex: function() {
return CKEDITOR.tools.indexOf( this.parent.children, this );
},
getFilterContext: function( context ) {
return context || {};
}
};
} )();

View File

@ -0,0 +1,70 @@
/**
* @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
'use strict';
( function() {
/**
* A lightweight representation of HTML text.
*
* @class
* @extends CKEDITOR.htmlParser.node
* @constructor Creates a text class instance.
* @param {String} value The text node value.
*/
CKEDITOR.htmlParser.text = function( value ) {
/**
* The text value.
*
* @property {String}
*/
this.value = value;
/** @private */
this._ = {
isBlockLike: false
};
};
CKEDITOR.htmlParser.text.prototype = CKEDITOR.tools.extend( new CKEDITOR.htmlParser.node(), {
/**
* The node type. This is a constant value set to {@link CKEDITOR#NODE_TEXT}.
*
* @readonly
* @property {Number} [=CKEDITOR.NODE_TEXT]
*/
type: CKEDITOR.NODE_TEXT,
/**
* Filter this text node with given filter.
*
* @since 4.1
* @param {CKEDITOR.htmlParser.filter} filter
* @returns {Boolean} Method returns `false` when this text node has
* been removed. This is an information for {@link CKEDITOR.htmlParser.element#filterChildren}
* that it has to repeat filter on current position in parent's children array.
*/
filter: function( filter, context ) {
if ( !( this.value = filter.onText( context, this.value, this ) ) ) {
this.remove();
return false;
}
},
/**
* Writes the HTML representation of this text to a {CKEDITOR.htmlParser.basicWriter}.
*
* @param {CKEDITOR.htmlParser.basicWriter} writer The writer to which write the HTML.
* @param {CKEDITOR.htmlParser.filter} [filter] The filter to be applied to this node.
* **Note:** it's unsafe to filter offline (not appended) node.
*/
writeHtml: function( writer, filter ) {
if ( filter )
this.filter( filter );
writer.text( this.value );
}
} );
} )();

View File

@ -0,0 +1,189 @@
/**
* @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
CKEDITOR.plugins.add( 'basicstyles', {
lang: 'af,ar,bg,bn,bs,ca,cs,cy,da,de,el,en,en-au,en-ca,en-gb,eo,es,et,eu,fa,fi,fo,fr,fr-ca,gl,gu,he,hi,hr,hu,id,is,it,ja,ka,km,ko,ku,lt,lv,mk,mn,ms,nb,nl,no,pl,pt,pt-br,ro,ru,si,sk,sl,sq,sr,sr-latn,sv,th,tr,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE%
icons: 'bold,italic,underline,strike,subscript,superscript', // %REMOVE_LINE_CORE%
hidpi: true, // %REMOVE_LINE_CORE%
init: function( editor ) {
var order = 0;
// All buttons use the same code to register. So, to avoid
// duplications, let's use this tool function.
var addButtonCommand = function( buttonName, buttonLabel, commandName, styleDefiniton ) {
// Disable the command if no definition is configured.
if ( !styleDefiniton )
return;
var style = new CKEDITOR.style( styleDefiniton ),
forms = contentForms[ commandName ];
// Put the style as the most important form.
forms.unshift( style );
// Listen to contextual style activation.
editor.attachStyleStateChange( style, function( state ) {
!editor.readOnly && editor.getCommand( commandName ).setState( state );
} );
// Create the command that can be used to apply the style.
editor.addCommand( commandName, new CKEDITOR.styleCommand( style, {
contentForms: forms
} ) );
// Register the button, if the button plugin is loaded.
if ( editor.ui.addButton ) {
editor.ui.addButton( buttonName, {
label: buttonLabel,
command: commandName,
toolbar: 'basicstyles,' + ( order += 10 )
} );
}
};
var contentForms = {
bold: [
'strong',
'b',
[ 'span', function( el ) {
var fw = el.styles[ 'font-weight' ];
return fw == 'bold' || +fw >= 700;
} ]
],
italic: [
'em',
'i',
[ 'span', function( el ) {
return el.styles[ 'font-style' ] == 'italic';
} ]
],
underline: [
'u',
[ 'span', function( el ) {
return el.styles[ 'text-decoration' ] == 'underline';
} ]
],
strike: [
's',
'strike',
[ 'span', function( el ) {
return el.styles[ 'text-decoration' ] == 'line-through';
} ]
],
subscript: [
'sub'
],
superscript: [
'sup'
]
},
config = editor.config,
lang = editor.lang.basicstyles;
addButtonCommand( 'Bold', lang.bold, 'bold', config.coreStyles_bold );
addButtonCommand( 'Italic', lang.italic, 'italic', config.coreStyles_italic );
addButtonCommand( 'Underline', lang.underline, 'underline', config.coreStyles_underline );
addButtonCommand( 'Strike', lang.strike, 'strike', config.coreStyles_strike );
addButtonCommand( 'Subscript', lang.subscript, 'subscript', config.coreStyles_subscript );
addButtonCommand( 'Superscript', lang.superscript, 'superscript', config.coreStyles_superscript );
editor.setKeystroke( [
[ CKEDITOR.CTRL + 66 /*B*/, 'bold' ],
[ CKEDITOR.CTRL + 73 /*I*/, 'italic' ],
[ CKEDITOR.CTRL + 85 /*U*/, 'underline' ]
] );
}
} );
// Basic Inline Styles.
/**
* The style definition that applies the **bold** style to the text.
*
* config.coreStyles_bold = { element: 'b', overrides: 'strong' };
*
* config.coreStyles_bold = {
* element: 'span',
* attributes: { 'class': 'Bold' }
* };
*
* @cfg
* @member CKEDITOR.config
*/
CKEDITOR.config.coreStyles_bold = { element: 'strong', overrides: 'b' };
/**
* The style definition that applies the *italics* style to the text.
*
* config.coreStyles_italic = { element: 'i', overrides: 'em' };
*
* CKEDITOR.config.coreStyles_italic = {
* element: 'span',
* attributes: { 'class': 'Italic' }
* };
*
* @cfg
* @member CKEDITOR.config
*/
CKEDITOR.config.coreStyles_italic = { element: 'em', overrides: 'i' };
/**
* The style definition that applies the <u>underline</u> style to the text.
*
* CKEDITOR.config.coreStyles_underline = {
* element: 'span',
* attributes: { 'class': 'Underline' }
* };
*
* @cfg
* @member CKEDITOR.config
*/
CKEDITOR.config.coreStyles_underline = { element: 'u' };
/**
* The style definition that applies the <strike>strike-through</strike> style to the text.
*
* CKEDITOR.config.coreStyles_strike = {
* element: 'span',
* attributes: { 'class': 'StrikeThrough' },
* overrides: 'strike'
* };
*
* @cfg
* @member CKEDITOR.config
*/
CKEDITOR.config.coreStyles_strike = { element: 's', overrides: 'strike' };
/**
* The style definition that applies the subscript style to the text.
*
* CKEDITOR.config.coreStyles_subscript = {
* element: 'span',
* attributes: { 'class': 'Subscript' },
* overrides: 'sub'
* };
*
* @cfg
* @member CKEDITOR.config
*/
CKEDITOR.config.coreStyles_subscript = { element: 'sub' };
/**
* The style definition that applies the superscript style to the text.
*
* CKEDITOR.config.coreStyles_superscript = {
* element: 'span',
* attributes: { 'class': 'Superscript' },
* overrides: 'sup'
* };
*
* @cfg
* @member CKEDITOR.config
*/
CKEDITOR.config.coreStyles_superscript = { element: 'sup' };

View File

@ -0,0 +1,248 @@
/**
* @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
( function() {
function noBlockLeft( bqBlock ) {
for ( var i = 0, length = bqBlock.getChildCount(), child; i < length && ( child = bqBlock.getChild( i ) ); i++ ) {
if ( child.type == CKEDITOR.NODE_ELEMENT && child.isBlockBoundary() )
return false;
}
return true;
}
var commandObject = {
exec: function( editor ) {
var state = editor.getCommand( 'blockquote' ).state,
selection = editor.getSelection(),
range = selection && selection.getRanges()[ 0 ];
if ( !range )
return;
var bookmarks = selection.createBookmarks();
// Kludge for #1592: if the bookmark nodes are in the beginning of
// blockquote, then move them to the nearest block element in the
// blockquote.
if ( CKEDITOR.env.ie ) {
var bookmarkStart = bookmarks[ 0 ].startNode,
bookmarkEnd = bookmarks[ 0 ].endNode,
cursor;
if ( bookmarkStart && bookmarkStart.getParent().getName() == 'blockquote' ) {
cursor = bookmarkStart;
while ( ( cursor = cursor.getNext() ) ) {
if ( cursor.type == CKEDITOR.NODE_ELEMENT && cursor.isBlockBoundary() ) {
bookmarkStart.move( cursor, true );
break;
}
}
}
if ( bookmarkEnd && bookmarkEnd.getParent().getName() == 'blockquote' ) {
cursor = bookmarkEnd;
while ( ( cursor = cursor.getPrevious() ) ) {
if ( cursor.type == CKEDITOR.NODE_ELEMENT && cursor.isBlockBoundary() ) {
bookmarkEnd.move( cursor );
break;
}
}
}
}
var iterator = range.createIterator(),
block;
iterator.enlargeBr = editor.config.enterMode != CKEDITOR.ENTER_BR;
if ( state == CKEDITOR.TRISTATE_OFF ) {
var paragraphs = [];
while ( ( block = iterator.getNextParagraph() ) )
paragraphs.push( block );
// If no paragraphs, create one from the current selection position.
if ( paragraphs.length < 1 ) {
var para = editor.document.createElement( editor.config.enterMode == CKEDITOR.ENTER_P ? 'p' : 'div' ),
firstBookmark = bookmarks.shift();
range.insertNode( para );
para.append( new CKEDITOR.dom.text( '\ufeff', editor.document ) );
range.moveToBookmark( firstBookmark );
range.selectNodeContents( para );
range.collapse( true );
firstBookmark = range.createBookmark();
paragraphs.push( para );
bookmarks.unshift( firstBookmark );
}
// Make sure all paragraphs have the same parent.
var commonParent = paragraphs[ 0 ].getParent(),
tmp = [];
for ( var i = 0; i < paragraphs.length; i++ ) {
block = paragraphs[ i ];
commonParent = commonParent.getCommonAncestor( block.getParent() );
}
// The common parent must not be the following tags: table, tbody, tr, ol, ul.
var denyTags = { table: 1, tbody: 1, tr: 1, ol: 1, ul: 1 };
while ( denyTags[ commonParent.getName() ] )
commonParent = commonParent.getParent();
// Reconstruct the block list to be processed such that all resulting blocks
// satisfy parentNode.equals( commonParent ).
var lastBlock = null;
while ( paragraphs.length > 0 ) {
block = paragraphs.shift();
while ( !block.getParent().equals( commonParent ) )
block = block.getParent();
if ( !block.equals( lastBlock ) )
tmp.push( block );
lastBlock = block;
}
// If any of the selected blocks is a blockquote, remove it to prevent
// nested blockquotes.
while ( tmp.length > 0 ) {
block = tmp.shift();
if ( block.getName() == 'blockquote' ) {
var docFrag = new CKEDITOR.dom.documentFragment( editor.document );
while ( block.getFirst() ) {
docFrag.append( block.getFirst().remove() );
paragraphs.push( docFrag.getLast() );
}
docFrag.replace( block );
} else
paragraphs.push( block );
}
// Now we have all the blocks to be included in a new blockquote node.
var bqBlock = editor.document.createElement( 'blockquote' );
bqBlock.insertBefore( paragraphs[ 0 ] );
while ( paragraphs.length > 0 ) {
block = paragraphs.shift();
bqBlock.append( block );
}
} else if ( state == CKEDITOR.TRISTATE_ON ) {
var moveOutNodes = [],
database = {};
while ( ( block = iterator.getNextParagraph() ) ) {
var bqParent = null,
bqChild = null;
while ( block.getParent() ) {
if ( block.getParent().getName() == 'blockquote' ) {
bqParent = block.getParent();
bqChild = block;
break;
}
block = block.getParent();
}
// Remember the blocks that were recorded down in the moveOutNodes array
// to prevent duplicates.
if ( bqParent && bqChild && !bqChild.getCustomData( 'blockquote_moveout' ) ) {
moveOutNodes.push( bqChild );
CKEDITOR.dom.element.setMarker( database, bqChild, 'blockquote_moveout', true );
}
}
CKEDITOR.dom.element.clearAllMarkers( database );
var movedNodes = [],
processedBlockquoteBlocks = [];
database = {};
while ( moveOutNodes.length > 0 ) {
var node = moveOutNodes.shift();
bqBlock = node.getParent();
// If the node is located at the beginning or the end, just take it out
// without splitting. Otherwise, split the blockquote node and move the
// paragraph in between the two blockquote nodes.
if ( !node.getPrevious() )
node.remove().insertBefore( bqBlock );
else if ( !node.getNext() )
node.remove().insertAfter( bqBlock );
else {
node.breakParent( node.getParent() );
processedBlockquoteBlocks.push( node.getNext() );
}
// Remember the blockquote node so we can clear it later (if it becomes empty).
if ( !bqBlock.getCustomData( 'blockquote_processed' ) ) {
processedBlockquoteBlocks.push( bqBlock );
CKEDITOR.dom.element.setMarker( database, bqBlock, 'blockquote_processed', true );
}
movedNodes.push( node );
}
CKEDITOR.dom.element.clearAllMarkers( database );
// Clear blockquote nodes that have become empty.
for ( i = processedBlockquoteBlocks.length - 1; i >= 0; i-- ) {
bqBlock = processedBlockquoteBlocks[ i ];
if ( noBlockLeft( bqBlock ) )
bqBlock.remove();
}
if ( editor.config.enterMode == CKEDITOR.ENTER_BR ) {
var firstTime = true;
while ( movedNodes.length ) {
node = movedNodes.shift();
if ( node.getName() == 'div' ) {
docFrag = new CKEDITOR.dom.documentFragment( editor.document );
var needBeginBr = firstTime && node.getPrevious() && !( node.getPrevious().type == CKEDITOR.NODE_ELEMENT && node.getPrevious().isBlockBoundary() );
if ( needBeginBr )
docFrag.append( editor.document.createElement( 'br' ) );
var needEndBr = node.getNext() && !( node.getNext().type == CKEDITOR.NODE_ELEMENT && node.getNext().isBlockBoundary() );
while ( node.getFirst() )
node.getFirst().remove().appendTo( docFrag );
if ( needEndBr )
docFrag.append( editor.document.createElement( 'br' ) );
docFrag.replace( node );
firstTime = false;
}
}
}
}
selection.selectBookmarks( bookmarks );
editor.focus();
},
refresh: function( editor, path ) {
// Check if inside of blockquote.
var firstBlock = path.block || path.blockLimit;
this.setState( editor.elementPath( firstBlock ).contains( 'blockquote', 1 ) ? CKEDITOR.TRISTATE_ON : CKEDITOR.TRISTATE_OFF );
},
context: 'blockquote',
allowedContent: 'blockquote',
requiredContent: 'blockquote'
};
CKEDITOR.plugins.add( 'blockquote', {
lang: 'af,ar,bg,bn,bs,ca,cs,cy,da,de,el,en,en-au,en-ca,en-gb,eo,es,et,eu,fa,fi,fo,fr,fr-ca,gl,gu,he,hi,hr,hu,id,is,it,ja,ka,km,ko,ku,lt,lv,mk,mn,ms,nb,nl,no,pl,pt,pt-br,ro,ru,si,sk,sl,sq,sr,sr-latn,sv,th,tr,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE%
icons: 'blockquote', // %REMOVE_LINE_CORE%
hidpi: true, // %REMOVE_LINE_CORE%
init: function( editor ) {
if ( editor.blockless )
return;
editor.addCommand( 'blockquote', commandObject );
editor.ui.addButton && editor.ui.addButton( 'Blockquote', {
label: editor.lang.blockquote.toolbar,
command: 'blockquote',
toolbar: 'blocks,10'
} );
}
} );
} )();

View File

@ -0,0 +1,379 @@
/**
* @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
( function() {
var template = '<a id="{id}"' +
' class="cke_button cke_button__{name} cke_button_{state} {cls}"' +
( CKEDITOR.env.gecko && CKEDITOR.env.version >= 10900 && !CKEDITOR.env.hc ? '' : ' href="javascript:void(\'{titleJs}\')"' ) +
' title="{title}"' +
' tabindex="-1"' +
' hidefocus="true"' +
' role="button"' +
' aria-labelledby="{id}_label"' +
' aria-haspopup="{hasArrow}"' +
' aria-disabled="{ariaDisabled}"';
// Some browsers don't cancel key events in the keydown but in the
// keypress.
// TODO: Check if really needed for Gecko+Mac.
if ( CKEDITOR.env.opera || ( CKEDITOR.env.gecko && CKEDITOR.env.mac ) )
template += ' onkeypress="return false;"';
// With Firefox, we need to force the button to redraw, otherwise it
// will remain in the focus state.
if ( CKEDITOR.env.gecko )
template += ' onblur="this.style.cssText = this.style.cssText;"';
template += ' onkeydown="return CKEDITOR.tools.callFunction({keydownFn},event);"' +
' onfocus="return CKEDITOR.tools.callFunction({focusFn},event);" ' +
' onmousedown="return CKEDITOR.tools.callFunction({mousedownFn},event);" ' +
( CKEDITOR.env.ie ? 'onclick="return false;" onmouseup' : 'onclick' ) + // #188
'="CKEDITOR.tools.callFunction({clickFn},this);return false;">' +
'<span class="cke_button_icon cke_button__{iconName}_icon" style="{style}"';
template += '>&nbsp;</span>' +
'<span id="{id}_label" class="cke_button_label cke_button__{name}_label" aria-hidden="false">{label}</span>' +
'{arrowHtml}' +
'</a>';
var templateArrow = '<span class="cke_button_arrow">' +
// BLACK DOWN-POINTING TRIANGLE
( CKEDITOR.env.hc ? '&#9660;' : '' ) +
'</span>';
var btnArrowTpl = CKEDITOR.addTemplate( 'buttonArrow', templateArrow ),
btnTpl = CKEDITOR.addTemplate( 'button', template );
CKEDITOR.plugins.add( 'button', {
lang: 'ca,cs,el,en,en-gb,fa,fr,gl,hu,ja,km,nl,pl,pt,pt-br,ru,sl,sv,uk,zh-cn', // %REMOVE_LINE_CORE%
beforeInit: function( editor ) {
editor.ui.addHandler( CKEDITOR.UI_BUTTON, CKEDITOR.ui.button.handler );
}
} );
/**
* Button UI element.
*
* @readonly
* @property {String} [='button']
* @member CKEDITOR
*/
CKEDITOR.UI_BUTTON = 'button';
/**
* Represents a button UI element. This class should not be called directly. To
* create new buttons use {@link CKEDITOR.ui#addButton} instead.
*
* @class
* @constructor Creates a button class instance.
* @param {Object} definition The button definition.
*/
CKEDITOR.ui.button = function( definition ) {
CKEDITOR.tools.extend( this, definition,
// Set defaults.
{
title: definition.label,
click: definition.click ||
function( editor ) {
editor.execCommand( definition.command );
}
} );
this._ = {};
};
/**
* Represents button handler object.
*
* @class
* @singleton
* @extends CKEDITOR.ui.handlerDefinition
*/
CKEDITOR.ui.button.handler = {
/**
* Transforms a button definition in a {@link CKEDITOR.ui.button} instance.
*
* @member CKEDITOR.ui.button.handler
* @param {Object} definition
* @returns {CKEDITOR.ui.button}
*/
create: function( definition ) {
return new CKEDITOR.ui.button( definition );
}
};
/** @class CKEDITOR.ui.button */
CKEDITOR.ui.button.prototype = {
/**
* Renders the button.
*
* @param {CKEDITOR.editor} editor The editor instance which this button is
* to be used by.
* @param {Array} output The output array to which append the HTML relative
* to this button.
*/
render: function( editor, output ) {
var env = CKEDITOR.env,
id = this._.id = CKEDITOR.tools.getNextId(),
stateName = '',
command = this.command,
// Get the command name.
clickFn;
this._.editor = editor;
var instance = {
id: id,
button: this,
editor: editor,
focus: function() {
var element = CKEDITOR.document.getById( id );
element.focus();
},
execute: function() {
this.button.click( editor );
},
attach: function( editor ) {
this.button.attach( editor );
}
};
var keydownFn = CKEDITOR.tools.addFunction( function( ev ) {
if ( instance.onkey ) {
ev = new CKEDITOR.dom.event( ev );
return ( instance.onkey( instance, ev.getKeystroke() ) !== false );
}
} );
var focusFn = CKEDITOR.tools.addFunction( function( ev ) {
var retVal;
if ( instance.onfocus )
retVal = ( instance.onfocus( instance, new CKEDITOR.dom.event( ev ) ) !== false );
// FF2: prevent focus event been bubbled up to editor container, which caused unexpected editor focus.
if ( CKEDITOR.env.gecko && CKEDITOR.env.version < 10900 )
ev.preventBubble();
return retVal;
} );
var selLocked = 0;
var mousedownFn = CKEDITOR.tools.addFunction( function() {
// Opera: lock to prevent loosing editable text selection when clicking on button.
if ( CKEDITOR.env.opera ) {
var edt = editor.editable();
if ( edt.isInline() && edt.hasFocus ) {
editor.lockSelection();
selLocked = 1;
}
}
} );
instance.clickFn = clickFn = CKEDITOR.tools.addFunction( function() {
// Restore locked selection in Opera.
if ( selLocked ) {
editor.unlockSelection( 1 );
selLocked = 0;
}
instance.execute();
} );
// Indicate a mode sensitive button.
if ( this.modes ) {
var modeStates = {};
function updateState() {
// "this" is a CKEDITOR.ui.button instance.
var mode = editor.mode;
if ( mode ) {
// Restore saved button state.
var state = this.modes[ mode ] ? modeStates[ mode ] != undefined ? modeStates[ mode ] : CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED;
state = editor.readOnly && !this.readOnly ? CKEDITOR.TRISTATE_DISABLED : state;
this.setState( state );
// Let plugin to disable button.
if ( this.refresh )
this.refresh();
}
}
editor.on( 'beforeModeUnload', function() {
if ( editor.mode && this._.state != CKEDITOR.TRISTATE_DISABLED )
modeStates[ editor.mode ] = this._.state;
}, this );
// Update status when activeFilter, mode or readOnly changes.
editor.on( 'activeFilterChange', updateState, this );
editor.on( 'mode', updateState, this );
// If this button is sensitive to readOnly state, update it accordingly.
!this.readOnly && editor.on( 'readOnly', updateState, this );
} else if ( command ) {
// Get the command instance.
command = editor.getCommand( command );
if ( command ) {
command.on( 'state', function() {
this.setState( command.state );
}, this );
stateName += ( command.state == CKEDITOR.TRISTATE_ON ? 'on' : command.state == CKEDITOR.TRISTATE_DISABLED ? 'disabled' : 'off' );
}
}
// For button that has text-direction awareness on selection path.
if ( this.directional ) {
editor.on( 'contentDirChanged', function( evt ) {
var el = CKEDITOR.document.getById( this._.id ),
icon = el.getFirst();
var pathDir = evt.data;
// Make a minor direction change to become style-able for the skin icon.
if ( pathDir != editor.lang.dir )
el.addClass( 'cke_' + pathDir );
else
el.removeClass( 'cke_ltr' ).removeClass( 'cke_rtl' );
// Inline style update for the plugin icon.
icon.setAttribute( 'style', CKEDITOR.skin.getIconStyle( iconName, pathDir == 'rtl', this.icon, this.iconOffset ) );
}, this );
}
if ( !command )
stateName += 'off';
var name = this.name || this.command,
iconName = name;
// Check if we're pointing to an icon defined by another command. (#9555)
if ( this.icon && !( /\./ ).test( this.icon ) ) {
iconName = this.icon;
this.icon = null;
}
var params = {
id: id,
name: name,
iconName: iconName,
label: this.label,
cls: this.className || '',
state: stateName,
ariaDisabled: stateName == 'disabled' ? 'true' : 'false',
title: this.title,
titleJs: env.gecko && env.version >= 10900 && !env.hc ? '' : ( this.title || '' ).replace( "'", '' ),
hasArrow: this.hasArrow ? 'true' : 'false',
keydownFn: keydownFn,
mousedownFn: mousedownFn,
focusFn: focusFn,
clickFn: clickFn,
style: CKEDITOR.skin.getIconStyle( iconName, ( editor.lang.dir == 'rtl' ), this.icon, this.iconOffset ),
arrowHtml: this.hasArrow ? btnArrowTpl.output() : ''
};
btnTpl.output( params, output );
if ( this.onRender )
this.onRender();
return instance;
},
/**
* @todo
*/
setState: function( state ) {
if ( this._.state == state )
return false;
this._.state = state;
var element = CKEDITOR.document.getById( this._.id );
if ( element ) {
element.setState( state, 'cke_button' );
state == CKEDITOR.TRISTATE_DISABLED ?
element.setAttribute( 'aria-disabled', true ) :
element.removeAttribute( 'aria-disabled' );
if ( !this.hasArrow ) {
// Note: aria-pressed attribute should not be added to menuButton instances. (#11331)
state == CKEDITOR.TRISTATE_ON ?
element.setAttribute( 'aria-pressed', true ) :
element.removeAttribute( 'aria-pressed' );
} else {
var newLabel = state == CKEDITOR.TRISTATE_ON ?
this._.editor.lang.button.selectedLabel.replace( /%1/g, this.label ) : this.label;
CKEDITOR.document.getById( this._.id + '_label' ).setText( newLabel );
}
return true;
} else
return false;
},
/**
* @todo
*/
getState: function( state ) {
return this._.state;
},
/**
* Returns this button's {@link CKEDITOR.feature} instance.
*
* It may be this button instance if it has at least one of
* `allowedContent` and `requiredContent` properties. Otherwise,
* if command is bound to this button by `command` property, then
* that command will be returned.
*
* This method implements {@link CKEDITOR.feature#toFeature} interface method.
*
* @since 4.1
* @param {CKEDITOR.editor} Editor instance.
* @returns {CKEDITOR.feature} The feature.
*/
toFeature: function( editor ) {
if ( this._.feature )
return this._.feature;
var feature = this;
// If button isn't a feature, return command if is bound.
if ( !this.allowedContent && !this.requiredContent && this.command )
feature = editor.getCommand( this.command ) || feature;
return this._.feature = feature;
}
};
/**
* Adds a button definition to the UI elements list.
*
* editorInstance.ui.addButton( 'MyBold', {
* label: 'My Bold',
* command: 'bold',
* toolbar: 'basicstyles,1'
* } );
*
* @member CKEDITOR.ui
* @param {String} name The button name.
* @param {Object} definition The button definition.
*/
CKEDITOR.ui.prototype.addButton = function( name, definition ) {
this.add( name, CKEDITOR.UI_BUTTON, definition );
};
} )();

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,286 @@
/**
* @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
/**
* @fileOverview The "colorbutton" plugin that makes it possible to assign
* text and background colors to editor contents.
*
*/
CKEDITOR.plugins.add( 'colorbutton', {
requires: 'panelbutton,floatpanel',
lang: 'af,ar,bg,bn,bs,ca,cs,cy,da,de,el,en,en-au,en-ca,en-gb,eo,es,et,eu,fa,fi,fo,fr,fr-ca,gl,gu,he,hi,hr,hu,id,is,it,ja,ka,km,ko,ku,lt,lv,mk,mn,ms,nb,nl,no,pl,pt,pt-br,ro,ru,si,sk,sl,sq,sr,sr-latn,sv,th,tr,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE%
icons: 'bgcolor,textcolor', // %REMOVE_LINE_CORE%
hidpi: true, // %REMOVE_LINE_CORE%
init: function( editor ) {
var config = editor.config,
lang = editor.lang.colorbutton;
var clickFn;
if ( !CKEDITOR.env.hc ) {
addButton( 'TextColor', 'fore', lang.textColorTitle, 10 );
addButton( 'BGColor', 'back', lang.bgColorTitle, 20 );
}
function addButton( name, type, title, order ) {
var style = new CKEDITOR.style( config[ 'colorButton_' + type + 'Style' ] ),
colorBoxId = CKEDITOR.tools.getNextId() + '_colorBox';
editor.ui.add( name, CKEDITOR.UI_PANELBUTTON, {
label: title,
title: title,
modes: { wysiwyg: 1 },
editorFocus: 0,
toolbar: 'colors,' + order,
allowedContent: style,
requiredContent: style,
panel: {
css: CKEDITOR.skin.getPath( 'editor' ),
attributes: { role: 'listbox', 'aria-label': lang.panelTitle }
},
onBlock: function( panel, block ) {
block.autoSize = true;
block.element.addClass( 'cke_colorblock' );
block.element.setHtml( renderColors( panel, type, colorBoxId ) );
// The block should not have scrollbars (#5933, #6056)
block.element.getDocument().getBody().setStyle( 'overflow', 'hidden' );
CKEDITOR.ui.fire( 'ready', this );
var keys = block.keys;
var rtl = editor.lang.dir == 'rtl';
keys[ rtl ? 37 : 39 ] = 'next'; // ARROW-RIGHT
keys[ 40 ] = 'next'; // ARROW-DOWN
keys[ 9 ] = 'next'; // TAB
keys[ rtl ? 39 : 37 ] = 'prev'; // ARROW-LEFT
keys[ 38 ] = 'prev'; // ARROW-UP
keys[ CKEDITOR.SHIFT + 9 ] = 'prev'; // SHIFT + TAB
keys[ 32 ] = 'click'; // SPACE
},
refresh: function() {
if ( !editor.activeFilter.check( style ) )
this.setState( CKEDITOR.TRISTATE_DISABLED );
},
// The automatic colorbox should represent the real color (#6010)
onOpen: function() {
var selection = editor.getSelection(),
block = selection && selection.getStartElement(),
path = editor.elementPath( block ),
color;
if ( !path )
return;
// Find the closest block element.
block = path.block || path.blockLimit || editor.document.getBody();
// The background color might be transparent. In that case, look up the color in the DOM tree.
do {
color = block && block.getComputedStyle( type == 'back' ? 'background-color' : 'color' ) || 'transparent';
}
while ( type == 'back' && color == 'transparent' && block && ( block = block.getParent() ) );
// The box should never be transparent.
if ( !color || color == 'transparent' )
color = '#ffffff';
this._.panel._.iframe.getFrameDocument().getById( colorBoxId ).setStyle( 'background-color', color );
return color;
}
} );
}
function renderColors( panel, type, colorBoxId ) {
var output = [],
colors = config.colorButton_colors.split( ',' );
var clickFn = CKEDITOR.tools.addFunction( function( color, type ) {
if ( color == '?' ) {
var applyColorStyle = arguments.callee;
function onColorDialogClose( evt ) {
this.removeListener( 'ok', onColorDialogClose );
this.removeListener( 'cancel', onColorDialogClose );
evt.name == 'ok' && applyColorStyle( this.getContentElement( 'picker', 'selectedColor' ).getValue(), type );
}
editor.openDialog( 'colordialog', function() {
this.on( 'ok', onColorDialogClose );
this.on( 'cancel', onColorDialogClose );
} );
return;
}
editor.focus();
panel.hide();
editor.fire( 'saveSnapshot' );
// Clean up any conflicting style within the range.
editor.removeStyle( new CKEDITOR.style( config[ 'colorButton_' + type + 'Style' ], { color: 'inherit' } ) );
if ( color ) {
var colorStyle = config[ 'colorButton_' + type + 'Style' ];
colorStyle.childRule = type == 'back' ?
function( element ) {
// It's better to apply background color as the innermost style. (#3599)
// Except for "unstylable elements". (#6103)
return isUnstylable( element );
} : function( element ) {
// Fore color style must be applied inside links instead of around it. (#4772,#6908)
return !( element.is( 'a' ) || element.getElementsByTag( 'a' ).count() ) || isUnstylable( element );
};
editor.applyStyle( new CKEDITOR.style( colorStyle, { color: color } ) );
}
editor.fire( 'saveSnapshot' );
} );
// Render the "Automatic" button.
output.push( '<a class="cke_colorauto" _cke_focus=1 hidefocus=true' +
' title="', lang.auto, '"' +
' onclick="CKEDITOR.tools.callFunction(', clickFn, ',null,\'', type, '\');return false;"' +
' href="javascript:void(\'', lang.auto, '\')"' +
' role="option">' +
'<table role="presentation" cellspacing=0 cellpadding=0 width="100%">' +
'<tr>' +
'<td>' +
'<span class="cke_colorbox" id="', colorBoxId, '"></span>' +
'</td>' +
'<td colspan=7 align=center>', lang.auto, '</td>' +
'</tr>' +
'</table>' +
'</a>' +
'<table role="presentation" cellspacing=0 cellpadding=0 width="100%">' );
// Render the color boxes.
for ( var i = 0; i < colors.length; i++ ) {
if ( ( i % 8 ) === 0 )
output.push( '</tr><tr>' );
var parts = colors[ i ].split( '/' ),
colorName = parts[ 0 ],
colorCode = parts[ 1 ] || colorName;
// The data can be only a color code (without #) or colorName + color code
// If only a color code is provided, then the colorName is the color with the hash
// Convert the color from RGB to RRGGBB for better compatibility with IE and <font>. See #5676
if ( !parts[ 1 ] )
colorName = '#' + colorName.replace( /^(.)(.)(.)$/, '$1$1$2$2$3$3' );
var colorLabel = editor.lang.colorbutton.colors[ colorCode ] || colorCode;
output.push( '<td>' +
'<a class="cke_colorbox" _cke_focus=1 hidefocus=true' +
' title="', colorLabel, '"' +
' onclick="CKEDITOR.tools.callFunction(', clickFn, ',\'', colorName, '\',\'', type, '\'); return false;"' +
' href="javascript:void(\'', colorLabel, '\')"' +
' role="option">' +
'<span class="cke_colorbox" style="background-color:#', colorCode, '"></span>' +
'</a>' +
'</td>' );
}
// Render the "More Colors" button.
if ( editor.plugins.colordialog && config.colorButton_enableMore === undefined || config.colorButton_enableMore ) {
output.push( '</tr>' +
'<tr>' +
'<td colspan=8 align=center>' +
'<a class="cke_colormore" _cke_focus=1 hidefocus=true' +
' title="', lang.more, '"' +
' onclick="CKEDITOR.tools.callFunction(', clickFn, ',\'?\',\'', type, '\');return false;"' +
' href="javascript:void(\'', lang.more, '\')"', ' role="option">', lang.more, '</a>' +
'</td>' ); // tr is later in the code.
}
output.push( '</tr></table>' );
return output.join( '' );
}
function isUnstylable( ele ) {
return ( ele.getAttribute( 'contentEditable' ) == 'false' ) || ele.getAttribute( 'data-nostyle' );
}
}
} );
/**
* Whether to enable the **More Colors*** button in the color selectors.
*
* config.colorButton_enableMore = false;
*
* @cfg {Boolean} [colorButton_enableMore=true]
* @member CKEDITOR.config
*/
/**
* Defines the colors to be displayed in the color selectors. This is a string
* containing hexadecimal notation for HTML colors, without the `'#'` prefix.
*
* **Since 3.3:** A color name may optionally be defined by prefixing the entries with
* a name and the slash character. For example, `'FontColor1/FF9900'` will be
* displayed as the color `#FF9900` in the selector, but will be output as `'FontColor1'`.
*
* // Brazil colors only.
* config.colorButton_colors = '00923E,F8C100,28166F';
*
* config.colorButton_colors = 'FontColor1/FF9900,FontColor2/0066CC,FontColor3/F00';
*
* @cfg {String} [colorButton_colors=see source]
* @member CKEDITOR.config
*/
CKEDITOR.config.colorButton_colors = '000,800000,8B4513,2F4F4F,008080,000080,4B0082,696969,' +
'B22222,A52A2A,DAA520,006400,40E0D0,0000CD,800080,808080,' +
'F00,FF8C00,FFD700,008000,0FF,00F,EE82EE,A9A9A9,' +
'FFA07A,FFA500,FFFF00,00FF00,AFEEEE,ADD8E6,DDA0DD,D3D3D3,' +
'FFF0F5,FAEBD7,FFFFE0,F0FFF0,F0FFFF,F0F8FF,E6E6FA,FFF';
/**
* Stores the style definition that applies the text foreground color.
*
* // This is actually the default value.
* config.colorButton_foreStyle = {
* element: 'span',
* styles: { color: '#(color)' }
* };
*
* @cfg [colorButton_foreStyle=see source]
* @member CKEDITOR.config
*/
CKEDITOR.config.colorButton_foreStyle = {
element: 'span',
styles: { 'color': '#(color)' },
overrides: [ {
element: 'font', attributes: { 'color': null }
} ]
};
/**
* Stores the style definition that applies the text background color.
*
* // This is actually the default value.
* config.colorButton_backStyle = {
* element: 'span',
* styles: { 'background-color': '#(color)' }
* };
*
* @cfg [colorButton_backStyle=see source]
* @member CKEDITOR.config
*/
CKEDITOR.config.colorButton_backStyle = {
element: 'span',
styles: { 'background-color': '#(color)' }
};

View File

@ -0,0 +1,143 @@
/**
* @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
CKEDITOR.plugins.add( 'contextmenu', {
requires: 'menu',
lang: 'af,ar,bg,bn,bs,ca,cs,cy,da,de,el,en,en-au,en-ca,en-gb,eo,es,et,eu,fa,fi,fo,fr,fr-ca,gl,gu,he,hi,hr,hu,id,is,it,ja,ka,km,ko,ku,lt,lv,mk,mn,ms,nb,nl,no,pl,pt,pt-br,ro,ru,si,sk,sl,sq,sr,sr-latn,sv,th,tr,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE%
// Make sure the base class (CKEDITOR.menu) is loaded before it (#3318).
onLoad: function() {
/**
* Class replacing the non-configurable native context menu with configurable CKEditor's equivalent.
*
* @class
* @extends CKEDITOR.menu
*/
CKEDITOR.plugins.contextMenu = CKEDITOR.tools.createClass( {
base: CKEDITOR.menu,
/**
* Creates the CKEDITOR.plugins.contextMenu class instance.
*
* @constructor
* @param {CKEDITOR.editor} editor
*/
$: function( editor ) {
this.base.call( this, editor, {
panel: {
className: 'cke_menu_panel',
attributes: {
'aria-label': editor.lang.contextmenu.options
}
}
} );
},
proto: {
/**
* Starts watching on native context menu triggers (option key, right click) on given element.
*
* @param {CKEDITOR.dom.element} element
* @param {Boolean} [nativeContextMenuOnCtrl] Whether to open native context menu if
* *Ctrl* key is hold on opening the context menu. See {@link CKEDITOR.config#browserContextMenuOnCtrl}.
*/
addTarget: function( element, nativeContextMenuOnCtrl ) {
element.on( 'contextmenu', function( event ) {
var domEvent = event.data,
isCtrlKeyDown =
// Safari on Windows always show 'ctrlKey' as true in 'contextmenu' event,
// which make this property unreliable. (#4826)
( CKEDITOR.env.webkit ? holdCtrlKey : ( CKEDITOR.env.mac ? domEvent.$.metaKey : domEvent.$.ctrlKey ) );
if ( nativeContextMenuOnCtrl && isCtrlKeyDown )
return;
// Cancel the browser context menu.
domEvent.preventDefault();
var doc = domEvent.getTarget().getDocument(),
offsetParent = domEvent.getTarget().getDocument().getDocumentElement(),
fromFrame = !doc.equals( CKEDITOR.document ),
scroll = doc.getWindow().getScrollPosition(),
offsetX = fromFrame ? domEvent.$.clientX : domEvent.$.pageX || scroll.x + domEvent.$.clientX,
offsetY = fromFrame ? domEvent.$.clientY : domEvent.$.pageY || scroll.y + domEvent.$.clientY;
CKEDITOR.tools.setTimeout( function() {
this.open( offsetParent, null, offsetX, offsetY );
// IE needs a short while to allow selection change before opening menu. (#7908)
}, CKEDITOR.env.ie ? 200 : 0, this );
}, this );
if ( CKEDITOR.env.webkit ) {
var holdCtrlKey,
onKeyDown = function( event ) {
holdCtrlKey = CKEDITOR.env.mac ? event.data.$.metaKey : event.data.$.ctrlKey;
},
resetOnKeyUp = function() {
holdCtrlKey = 0;
};
element.on( 'keydown', onKeyDown );
element.on( 'keyup', resetOnKeyUp );
element.on( 'contextmenu', resetOnKeyUp );
}
},
/**
* Opens context menu in given location. See the {@link CKEDITOR.menu#show} method.
*
* @param {CKEDITOR.dom.element} offsetParent
* @param {Number} [corner]
* @param {Number} [offsetX]
* @param {Number} [offsetY]
*/
open: function( offsetParent, corner, offsetX, offsetY ) {
this.editor.focus();
offsetParent = offsetParent || CKEDITOR.document.getDocumentElement();
// #9362: Force selection check to update commands' states in the new context.
this.editor.selectionChange( 1 );
this.show( offsetParent, corner, offsetX, offsetY );
}
}
} );
},
beforeInit: function( editor ) {
/**
* @readonly
* @property {CKEDITOR.plugins.contextMenu} contextMenu
* @member CKEDITOR.editor
*/
var contextMenu = editor.contextMenu = new CKEDITOR.plugins.contextMenu( editor );
editor.on( 'contentDom', function() {
contextMenu.addTarget( editor.editable(), editor.config.browserContextMenuOnCtrl !== false );
} );
editor.addCommand( 'contextMenu', {
exec: function() {
editor.contextMenu.open( editor.document.getBody() );
}
} );
editor.setKeystroke( CKEDITOR.SHIFT + 121 /*F10*/, 'contextMenu' );
editor.setKeystroke( CKEDITOR.CTRL + CKEDITOR.SHIFT + 121 /*F10*/, 'contextMenu' );
}
} );
/**
* Whether to show the browser native context menu when the *Ctrl* or
* *Meta* (Mac) key is pressed on opening the context menu with the
* right mouse button click or the *Menu* key.
*
* config.browserContextMenuOnCtrl = false;
*
* @since 3.0.2
* @cfg {Boolean} [browserContextMenuOnCtrl=true]
* @member CKEDITOR.config
*/

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,529 @@
/**
* @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
( function() {
CKEDITOR.plugins.add( 'enterkey', {
init: function( editor ) {
editor.addCommand( 'enter', {
modes: { wysiwyg: 1 },
editorFocus: false,
exec: function( editor ) {
enter( editor );
}
} );
editor.addCommand( 'shiftEnter', {
modes: { wysiwyg: 1 },
editorFocus: false,
exec: function( editor ) {
shiftEnter( editor );
}
} );
editor.setKeystroke( [
[ 13, 'enter' ],
[ CKEDITOR.SHIFT + 13, 'shiftEnter' ]
] );
}
} );
var whitespaces = CKEDITOR.dom.walker.whitespaces(),
bookmark = CKEDITOR.dom.walker.bookmark();
CKEDITOR.plugins.enterkey = {
enterBlock: function( editor, mode, range, forceMode ) {
// Get the range for the current selection.
range = range || getRange( editor );
// We may not have valid ranges to work on, like when inside a
// contenteditable=false element.
if ( !range )
return;
var doc = range.document;
var atBlockStart = range.checkStartOfBlock(),
atBlockEnd = range.checkEndOfBlock(),
path = editor.elementPath( range.startContainer ),
block = path.block,
// Determine the block element to be used.
blockTag = ( mode == CKEDITOR.ENTER_DIV ? 'div' : 'p' ),
newBlock;
// Exit the list when we're inside an empty list item block. (#5376)
if ( atBlockStart && atBlockEnd ) {
// Exit the list when we're inside an empty list item block. (#5376)
if ( block && ( block.is( 'li' ) || block.getParent().is( 'li' ) ) ) {
var blockParent = block.getParent(),
blockGrandParent = blockParent.getParent(),
firstChild = !block.hasPrevious(),
lastChild = !block.hasNext(),
selection = editor.getSelection(),
bookmarks = selection.createBookmarks(),
orgDir = block.getDirection( 1 ),
className = block.getAttribute( 'class' ),
style = block.getAttribute( 'style' ),
dirLoose = blockGrandParent.getDirection( 1 ) != orgDir,
enterMode = editor.enterMode,
needsBlock = enterMode != CKEDITOR.ENTER_BR || dirLoose || style || className,
child;
if ( blockGrandParent.is( 'li' ) ) {
// If block is the first or the last child of the parent
// list, degrade it and move to the outer list:
// before the parent list if block is first child and after
// the parent list if block is the last child, respectively.
//
// <ul> => <ul>
// <li> => <li>
// <ul> => <ul>
// <li>x</li> => <li>x</li>
// <li>^</li> => </ul>
// </ul> => </li>
// </li> => <li>^</li>
// </ul> => </ul>
//
// AND
//
// <ul> => <ul>
// <li> => <li>^</li>
// <ul> => <li>
// <li>^</li> => <ul>
// <li>x</li> => <li>x</li>
// </ul> => </ul>
// </li> => </li>
// </ul> => </ul>
if ( firstChild || lastChild )
block[ firstChild ? 'insertBefore' : 'insertAfter' ]( blockGrandParent );
// If the empty block is neither first nor last child
// then split the list and the block as an element
// of outer list.
//
// => <ul>
// => <li>
// <ul> => <ul>
// <li> => <li>x</li>
// <ul> => </ul>
// <li>x</li> => </li>
// <li>^</li> => <li>^</li>
// <li>y</li> => <li>
// </ul> => <ul>
// </li> => <li>y</li>
// </ul> => </ul>
// => </li>
// => </ul>
else
block.breakParent( blockGrandParent );
}
else if ( !needsBlock ) {
block.appendBogus( true );
// If block is the first or last child of the parent
// list, move all block's children out of the list:
// before the list if block is first child and after the list
// if block is the last child, respectively.
//
// <ul> => <ul>
// <li>x</li> => <li>x</li>
// <li>^</li> => </ul>
// </ul> => ^
//
// AND
//
// <ul> => ^
// <li>^</li> => <ul>
// <li>x</li> => <li>x</li>
// </ul> => </ul>
if ( firstChild || lastChild ) {
while ( ( child = block[ firstChild ? 'getFirst' : 'getLast' ]() ) )
child[ firstChild ? 'insertBefore' : 'insertAfter' ]( blockParent );
}
// If the empty block is neither first nor last child
// then split the list and put all the block contents
// between two lists.
//
// <ul> => <ul>
// <li>x</li> => <li>x</li>
// <li>^</li> => </ul>
// <li>y</li> => ^
// </ul> => <ul>
// => <li>y</li>
// => </ul>
else {
block.breakParent( blockParent );
while ( ( child = block.getLast() ) )
child.insertAfter( blockParent );
}
block.remove();
} else {
// Use <div> block for ENTER_BR and ENTER_DIV.
newBlock = doc.createElement( mode == CKEDITOR.ENTER_P ? 'p' : 'div' );
if ( dirLoose )
newBlock.setAttribute( 'dir', orgDir );
style && newBlock.setAttribute( 'style', style );
className && newBlock.setAttribute( 'class', className );
// Move all the child nodes to the new block.
block.moveChildren( newBlock );
// If block is the first or last child of the parent
// list, move it out of the list:
// before the list if block is first child and after the list
// if block is the last child, respectively.
//
// <ul> => <ul>
// <li>x</li> => <li>x</li>
// <li>^</li> => </ul>
// </ul> => <p>^</p>
//
// AND
//
// <ul> => <p>^</p>
// <li>^</li> => <ul>
// <li>x</li> => <li>x</li>
// </ul> => </ul>
if ( firstChild || lastChild )
newBlock[ firstChild ? 'insertBefore' : 'insertAfter' ]( blockParent );
// If the empty block is neither first nor last child
// then split the list and put the new block between
// two lists.
//
// => <ul>
// <ul> => <li>x</li>
// <li>x</li> => </ul>
// <li>^</li> => <p>^</p>
// <li>y</li> => <ul>
// </ul> => <li>y</li>
// => </ul>
else {
block.breakParent( blockParent );
newBlock.insertAfter( blockParent );
}
block.remove();
}
selection.selectBookmarks( bookmarks );
return;
}
if ( block && block.getParent().is( 'blockquote' ) ) {
block.breakParent( block.getParent() );
// If we were at the start of <blockquote>, there will be an empty element before it now.
if ( !block.getPrevious().getFirst( CKEDITOR.dom.walker.invisible( 1 ) ) )
block.getPrevious().remove();
// If we were at the end of <blockquote>, there will be an empty element after it now.
if ( !block.getNext().getFirst( CKEDITOR.dom.walker.invisible( 1 ) ) )
block.getNext().remove();
range.moveToElementEditStart( block );
range.select();
return;
}
}
// Don't split <pre> if we're in the middle of it, act as shift enter key.
else if ( block && block.is( 'pre' ) ) {
if ( !atBlockEnd ) {
enterBr( editor, mode, range, forceMode );
return;
}
}
// Split the range.
var splitInfo = range.splitBlock( blockTag );
if ( !splitInfo )
return;
// Get the current blocks.
var previousBlock = splitInfo.previousBlock,
nextBlock = splitInfo.nextBlock;
var isStartOfBlock = splitInfo.wasStartOfBlock,
isEndOfBlock = splitInfo.wasEndOfBlock;
var node;
// If this is a block under a list item, split it as well. (#1647)
if ( nextBlock ) {
node = nextBlock.getParent();
if ( node.is( 'li' ) ) {
nextBlock.breakParent( node );
nextBlock.move( nextBlock.getNext(), 1 );
}
} else if ( previousBlock && ( node = previousBlock.getParent() ) && node.is( 'li' ) ) {
previousBlock.breakParent( node );
node = previousBlock.getNext();
range.moveToElementEditStart( node );
previousBlock.move( previousBlock.getPrevious() );
}
// If we have both the previous and next blocks, it means that the
// boundaries were on separated blocks, or none of them where on the
// block limits (start/end).
if ( !isStartOfBlock && !isEndOfBlock ) {
// If the next block is an <li> with another list tree as the first
// child, we'll need to append a filler (<br>/NBSP) or the list item
// wouldn't be editable. (#1420)
if ( nextBlock.is( 'li' ) ) {
var walkerRange = range.clone();
walkerRange.selectNodeContents( nextBlock );
var walker = new CKEDITOR.dom.walker( walkerRange );
walker.evaluator = function( node ) {
return !( bookmark( node ) || whitespaces( node ) || node.type == CKEDITOR.NODE_ELEMENT && node.getName() in CKEDITOR.dtd.$inline && !( node.getName() in CKEDITOR.dtd.$empty ) );
};
node = walker.next();
if ( node && node.type == CKEDITOR.NODE_ELEMENT && node.is( 'ul', 'ol' ) )
( CKEDITOR.env.needsBrFiller ? doc.createElement( 'br' ) : doc.createText( '\xa0' ) ).insertBefore( node );
}
// Move the selection to the end block.
if ( nextBlock )
range.moveToElementEditStart( nextBlock );
} else {
var newBlockDir;
if ( previousBlock ) {
// Do not enter this block if it's a header tag, or we are in
// a Shift+Enter (#77). Create a new block element instead
// (later in the code).
if ( previousBlock.is( 'li' ) || !( headerTagRegex.test( previousBlock.getName() ) || previousBlock.is( 'pre' ) ) ) {
// Otherwise, duplicate the previous block.
newBlock = previousBlock.clone();
}
} else if ( nextBlock )
newBlock = nextBlock.clone();
if ( !newBlock ) {
// We have already created a new list item. (#6849)
if ( node && node.is( 'li' ) )
newBlock = node;
else {
newBlock = doc.createElement( blockTag );
if ( previousBlock && ( newBlockDir = previousBlock.getDirection() ) )
newBlock.setAttribute( 'dir', newBlockDir );
}
}
// Force the enter block unless we're talking of a list item.
else if ( forceMode && !newBlock.is( 'li' ) )
newBlock.renameNode( blockTag );
// Recreate the inline elements tree, which was available
// before hitting enter, so the same styles will be available in
// the new block.
var elementPath = splitInfo.elementPath;
if ( elementPath ) {
for ( var i = 0, len = elementPath.elements.length; i < len; i++ ) {
var element = elementPath.elements[ i ];
if ( element.equals( elementPath.block ) || element.equals( elementPath.blockLimit ) )
break;
if ( CKEDITOR.dtd.$removeEmpty[ element.getName() ] ) {
element = element.clone();
newBlock.moveChildren( element );
newBlock.append( element );
}
}
}
newBlock.appendBogus();
if ( !newBlock.getParent() )
range.insertNode( newBlock );
// list item start number should not be duplicated (#7330), but we need
// to remove the attribute after it's onto the DOM tree because of old IEs (#7581).
newBlock.is( 'li' ) && newBlock.removeAttribute( 'value' );
// This is tricky, but to make the new block visible correctly
// we must select it.
// The previousBlock check has been included because it may be
// empty if we have fixed a block-less space (like ENTER into an
// empty table cell).
if ( CKEDITOR.env.ie && isStartOfBlock && ( !isEndOfBlock || !previousBlock.getChildCount() ) ) {
// Move the selection to the new block.
range.moveToElementEditStart( isEndOfBlock ? previousBlock : newBlock );
range.select();
}
// Move the selection to the new block.
range.moveToElementEditStart( isStartOfBlock && !isEndOfBlock ? nextBlock : newBlock );
}
range.select();
range.scrollIntoView();
},
enterBr: function( editor, mode, range, forceMode ) {
// Get the range for the current selection.
range = range || getRange( editor );
// We may not have valid ranges to work on, like when inside a
// contenteditable=false element.
if ( !range )
return;
var doc = range.document;
// Determine the block element to be used.
var blockTag = ( mode == CKEDITOR.ENTER_DIV ? 'div' : 'p' );
var isEndOfBlock = range.checkEndOfBlock();
var elementPath = new CKEDITOR.dom.elementPath( editor.getSelection().getStartElement() );
var startBlock = elementPath.block,
startBlockTag = startBlock && elementPath.block.getName();
var isPre = false;
if ( !forceMode && startBlockTag == 'li' ) {
enterBlock( editor, mode, range, forceMode );
return;
}
// If we are at the end of a header block.
if ( !forceMode && isEndOfBlock && headerTagRegex.test( startBlockTag ) ) {
var newBlock, newBlockDir;
if ( ( newBlockDir = startBlock.getDirection() ) ) {
newBlock = doc.createElement( 'div' );
newBlock.setAttribute( 'dir', newBlockDir );
newBlock.insertAfter( startBlock );
range.setStart( newBlock, 0 );
} else {
// Insert a <br> after the current paragraph.
doc.createElement( 'br' ).insertAfter( startBlock );
// A text node is required by Gecko only to make the cursor blink.
if ( CKEDITOR.env.gecko )
doc.createText( '' ).insertAfter( startBlock );
// IE has different behaviors regarding position.
range.setStartAt( startBlock.getNext(), CKEDITOR.env.ie ? CKEDITOR.POSITION_BEFORE_START : CKEDITOR.POSITION_AFTER_START );
}
} else {
var lineBreak;
// IE<8 prefers text node as line-break inside of <pre> (#4711).
if ( startBlockTag == 'pre' && CKEDITOR.env.ie && CKEDITOR.env.version < 8 )
lineBreak = doc.createText( '\r' );
else
lineBreak = doc.createElement( 'br' );
range.deleteContents();
range.insertNode( lineBreak );
// Old IEs have different behavior regarding position.
if ( !CKEDITOR.env.needsBrFiller )
range.setStartAt( lineBreak, CKEDITOR.POSITION_AFTER_END );
else {
// A text node is required by Gecko only to make the cursor blink.
// We need some text inside of it, so the bogus <br> is properly
// created.
doc.createText( '\ufeff' ).insertAfter( lineBreak );
// If we are at the end of a block, we must be sure the bogus node is available in that block.
if ( isEndOfBlock )
lineBreak.getParent().appendBogus();
// Now we can remove the text node contents, so the caret doesn't
// stop on it.
lineBreak.getNext().$.nodeValue = '';
range.setStartAt( lineBreak.getNext(), CKEDITOR.POSITION_AFTER_START );
}
}
// This collapse guarantees the cursor will be blinking.
range.collapse( true );
range.select();
range.scrollIntoView();
}
};
var plugin = CKEDITOR.plugins.enterkey,
enterBr = plugin.enterBr,
enterBlock = plugin.enterBlock,
headerTagRegex = /^h[1-6]$/;
function shiftEnter( editor ) {
// On SHIFT+ENTER:
// 1. We want to enforce the mode to be respected, instead
// of cloning the current block. (#77)
return enter( editor, editor.activeShiftEnterMode, 1 );
}
function enter( editor, mode, forceMode ) {
forceMode = editor.config.forceEnterMode || forceMode;
// Only effective within document.
if ( editor.mode != 'wysiwyg' )
return;
if ( !mode )
mode = editor.activeEnterMode;
// TODO this should be handled by setting editor.activeEnterMode on selection change.
// Check path block specialities:
// 1. Cannot be a un-splittable element, e.g. table caption;
var path = editor.elementPath();
if ( !path.isContextFor( 'p' ) ) {
mode = CKEDITOR.ENTER_BR;
forceMode = 1;
}
editor.fire( 'saveSnapshot' ); // Save undo step.
if ( mode == CKEDITOR.ENTER_BR )
enterBr( editor, mode, null, forceMode );
else
enterBlock( editor, mode, null, forceMode );
editor.fire( 'saveSnapshot' );
}
function getRange( editor ) {
// Get the selection ranges.
var ranges = editor.getSelection().getRanges( true );
// Delete the contents of all ranges except the first one.
for ( var i = ranges.length - 1; i > 0; i-- ) {
ranges[ i ].deleteContents();
}
// Return the first range.
return ranges[ 0 ];
}
} )();

View File

@ -0,0 +1,239 @@
/**
* @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
( function() {
// Base HTML entities.
var htmlbase = 'nbsp,gt,lt,amp';
var entities =
// Latin-1 Entities
'quot,iexcl,cent,pound,curren,yen,brvbar,sect,uml,copy,ordf,laquo,' +
'not,shy,reg,macr,deg,plusmn,sup2,sup3,acute,micro,para,middot,' +
'cedil,sup1,ordm,raquo,frac14,frac12,frac34,iquest,times,divide,' +
// Symbols
'fnof,bull,hellip,prime,Prime,oline,frasl,weierp,image,real,trade,' +
'alefsym,larr,uarr,rarr,darr,harr,crarr,lArr,uArr,rArr,dArr,hArr,' +
'forall,part,exist,empty,nabla,isin,notin,ni,prod,sum,minus,lowast,' +
'radic,prop,infin,ang,and,or,cap,cup,int,there4,sim,cong,asymp,ne,' +
'equiv,le,ge,sub,sup,nsub,sube,supe,oplus,otimes,perp,sdot,lceil,' +
'rceil,lfloor,rfloor,lang,rang,loz,spades,clubs,hearts,diams,' +
// Other Special Characters
'circ,tilde,ensp,emsp,thinsp,zwnj,zwj,lrm,rlm,ndash,mdash,lsquo,' +
'rsquo,sbquo,ldquo,rdquo,bdquo,dagger,Dagger,permil,lsaquo,rsaquo,' +
'euro';
// Latin Letters Entities
var latin = 'Agrave,Aacute,Acirc,Atilde,Auml,Aring,AElig,Ccedil,Egrave,Eacute,' +
'Ecirc,Euml,Igrave,Iacute,Icirc,Iuml,ETH,Ntilde,Ograve,Oacute,Ocirc,' +
'Otilde,Ouml,Oslash,Ugrave,Uacute,Ucirc,Uuml,Yacute,THORN,szlig,' +
'agrave,aacute,acirc,atilde,auml,aring,aelig,ccedil,egrave,eacute,' +
'ecirc,euml,igrave,iacute,icirc,iuml,eth,ntilde,ograve,oacute,ocirc,' +
'otilde,ouml,oslash,ugrave,uacute,ucirc,uuml,yacute,thorn,yuml,' +
'OElig,oelig,Scaron,scaron,Yuml';
// Greek Letters Entities.
var greek = 'Alpha,Beta,Gamma,Delta,Epsilon,Zeta,Eta,Theta,Iota,Kappa,Lambda,Mu,' +
'Nu,Xi,Omicron,Pi,Rho,Sigma,Tau,Upsilon,Phi,Chi,Psi,Omega,alpha,' +
'beta,gamma,delta,epsilon,zeta,eta,theta,iota,kappa,lambda,mu,nu,xi,' +
'omicron,pi,rho,sigmaf,sigma,tau,upsilon,phi,chi,psi,omega,thetasym,' +
'upsih,piv';
// Create a mapping table between one character and its entity form from a list of entity names.
// @param reverse {Boolean} Whether to create a reverse map from the entity string form to an actual character.
function buildTable( entities, reverse ) {
var table = {},
regex = [];
// Entities that the browsers DOM don't transform to the final char
// automatically.
var specialTable = {
nbsp: '\u00A0', // IE | FF
shy: '\u00AD', // IE
gt: '\u003E', // IE | FF | -- | Opera
lt: '\u003C', // IE | FF | Safari | Opera
amp: '\u0026', // ALL
apos: '\u0027', // IE
quot: '\u0022' // IE
};
entities = entities.replace( /\b(nbsp|shy|gt|lt|amp|apos|quot)(?:,|$)/g, function( match, entity ) {
var org = reverse ? '&' + entity + ';' : specialTable[ entity ],
result = reverse ? specialTable[ entity ] : '&' + entity + ';';
table[ org ] = result;
regex.push( org );
return '';
} );
if ( !reverse && entities ) {
// Transforms the entities string into an array.
entities = entities.split( ',' );
// Put all entities inside a DOM element, transforming them to their
// final chars.
var div = document.createElement( 'div' ),
chars;
div.innerHTML = '&' + entities.join( ';&' ) + ';';
chars = div.innerHTML;
div = null;
// Add all chars to the table.
for ( var i = 0; i < chars.length; i++ ) {
var charAt = chars.charAt( i );
table[ charAt ] = '&' + entities[ i ] + ';';
regex.push( charAt );
}
}
table.regex = regex.join( reverse ? '|' : '' );
return table;
}
CKEDITOR.plugins.add( 'entities', {
afterInit: function( editor ) {
var config = editor.config;
var dataProcessor = editor.dataProcessor,
htmlFilter = dataProcessor && dataProcessor.htmlFilter;
if ( htmlFilter ) {
// Mandatory HTML base entities.
var selectedEntities = [];
if ( config.basicEntities !== false )
selectedEntities.push( htmlbase );
if ( config.entities ) {
if ( selectedEntities.length )
selectedEntities.push( entities );
if ( config.entities_latin )
selectedEntities.push( latin );
if ( config.entities_greek )
selectedEntities.push( greek );
if ( config.entities_additional )
selectedEntities.push( config.entities_additional );
}
var entitiesTable = buildTable( selectedEntities.join( ',' ) );
// Create the Regex used to find entities in the text, leave it matches nothing if entities are empty.
var entitiesRegex = entitiesTable.regex ? '[' + entitiesTable.regex + ']' : 'a^';
delete entitiesTable.regex;
if ( config.entities && config.entities_processNumerical )
entitiesRegex = '[^ -~]|' + entitiesRegex;
entitiesRegex = new RegExp( entitiesRegex, 'g' );
function getEntity( character ) {
return config.entities_processNumerical == 'force' || !entitiesTable[ character ] ? '&#' + character.charCodeAt( 0 ) + ';'
: entitiesTable[ character ];
}
// Decode entities that the browsers has transformed
// at first place.
var baseEntitiesTable = buildTable( [ htmlbase, 'shy' ].join( ',' ), true ),
baseEntitiesRegex = new RegExp( baseEntitiesTable.regex, 'g' );
function getChar( character ) {
return baseEntitiesTable[ character ];
}
htmlFilter.addRules( {
text: function( text ) {
return text.replace( baseEntitiesRegex, getChar ).replace( entitiesRegex, getEntity );
}
}, {
applyToAll: true,
excludeNestedEditable: true
} );
}
}
} );
} )();
/**
* Whether to escape basic HTML entities in the document, including:
*
* * `nbsp`
* * `gt`
* * `lt`
* * `amp`
*
* **Note:** It should not be subject to change unless when outputting a non-HTML data format like BBCode.
*
* config.basicEntities = false;
*
* @cfg {Boolean} [basicEntities=true]
* @member CKEDITOR.config
*/
CKEDITOR.config.basicEntities = true;
/**
* Whether to use HTML entities in the output.
*
* config.entities = false;
*
* @cfg {Boolean} [entities=true]
* @member CKEDITOR.config
*/
CKEDITOR.config.entities = true;
/**
* Whether to convert some Latin characters (Latin alphabet No. 1, ISO 8859-1)
* to HTML entities. The list of entities can be found in the
* [W3C HTML 4.01 Specification, section 24.2.1](http://www.w3.org/TR/html4/sgml/entities.html#h-24.2.1).
*
* config.entities_latin = false;
*
* @cfg {Boolean} [entities_latin=true]
* @member CKEDITOR.config
*/
CKEDITOR.config.entities_latin = true;
/**
* Whether to convert some symbols, mathematical symbols, and Greek letters to
* HTML entities. This may be more relevant for users typing text written in Greek.
* The list of entities can be found in the
* [W3C HTML 4.01 Specification, section 24.3.1(http://www.w3.org/TR/html4/sgml/entities.html#h-24.3.1).
*
* config.entities_greek = false;
*
* @cfg {Boolean} [entities_greek=true]
* @member CKEDITOR.config
*/
CKEDITOR.config.entities_greek = true;
/**
* Whether to convert all remaining characters not included in the ASCII
* character table to their relative decimal numeric representation of HTML entity.
* When set to `force`, it will convert all entities into this format.
*
* For example the phrase `'This is Chinese: 汉语.'` is output
* as `'This is Chinese: &#27721;&#35821;.'`
*
* config.entities_processNumerical = true;
* config.entities_processNumerical = 'force'; // Converts from '&nbsp;' into '&#160;';
*
* @cfg {Boolean/String} [entities_processNumerical=false]
* @member CKEDITOR.config
*/
/**
* A comma separated list of additional entities to be used. Entity names
* or numbers must be used in a form that excludes the `'&amp;'` prefix and the `';'` ending.
*
* config.entities_additional = '#1049'; // Adds Cyrillic capital letter Short I (Й).
*
* @cfg {String} [entities_additional='#39' (The single quote (') character)]
* @member CKEDITOR.config
*/
CKEDITOR.config.entities_additional = '#39';

View File

@ -0,0 +1,178 @@
/**
* @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
( function() {
var cssStyle = CKEDITOR.htmlParser.cssStyle,
cssLength = CKEDITOR.tools.cssLength;
var cssLengthRegex = /^((?:\d*(?:\.\d+))|(?:\d+))(.*)?$/i;
// Replacing the former CSS length value with the later one, with
// adjustment to the length unit.
function replaceCssLength( length1, length2 ) {
var parts1 = cssLengthRegex.exec( length1 ),
parts2 = cssLengthRegex.exec( length2 );
// Omit pixel length unit when necessary,
// e.g. replaceCssLength( 10, '20px' ) -> 20
if ( parts1 ) {
if ( !parts1[ 2 ] && parts2[ 2 ] == 'px' )
return parts2[ 1 ];
if ( parts1[ 2 ] == 'px' && !parts2[ 2 ] )
return parts2[ 1 ] + 'px';
}
return length2;
}
var htmlFilterRules = {
elements: {
$: function( element ) {
var attributes = element.attributes,
realHtml = attributes && attributes[ 'data-cke-realelement' ],
realFragment = realHtml && new CKEDITOR.htmlParser.fragment.fromHtml( decodeURIComponent( realHtml ) ),
realElement = realFragment && realFragment.children[ 0 ];
// Width/height in the fake object are subjected to clone into the real element.
if ( realElement && element.attributes[ 'data-cke-resizable' ] ) {
var styles = new cssStyle( element ).rules,
realAttrs = realElement.attributes,
width = styles.width,
height = styles.height;
width && ( realAttrs.width = replaceCssLength( realAttrs.width, width ) );
height && ( realAttrs.height = replaceCssLength( realAttrs.height, height ) );
}
return realElement;
}
}
};
var plugin = CKEDITOR.plugins.add( 'fakeobjects', {
lang: 'af,ar,bg,bn,bs,ca,cs,cy,da,de,el,en,en-au,en-ca,en-gb,eo,es,et,eu,fa,fi,fo,fr,fr-ca,gl,gu,he,hi,hr,hu,id,is,it,ja,ka,km,ko,ku,lt,lv,mk,mn,ms,nb,nl,no,pl,pt,pt-br,ro,ru,si,sk,sl,sq,sr,sr-latn,sv,th,tr,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE%
init: function( editor ) {
// Allow image with all styles and classes plus src, alt and title attributes.
// We need them when fakeobject is pasted.
editor.filter.allow( 'img[!data-cke-realelement,src,alt,title](*){*}', 'fakeobjects' );
},
afterInit: function( editor ) {
var dataProcessor = editor.dataProcessor,
htmlFilter = dataProcessor && dataProcessor.htmlFilter;
if ( htmlFilter )
htmlFilter.addRules( htmlFilterRules );
}
} );
/**
* @member CKEDITOR.editor
* @todo
*/
CKEDITOR.editor.prototype.createFakeElement = function( realElement, className, realElementType, isResizable ) {
var lang = this.lang.fakeobjects,
label = lang[ realElementType ] || lang.unknown;
var attributes = {
'class': className,
'data-cke-realelement': encodeURIComponent( realElement.getOuterHtml() ),
'data-cke-real-node-type': realElement.type,
alt: label,
title: label,
align: realElement.getAttribute( 'align' ) || ''
};
// Do not set "src" on high-contrast so the alt text is displayed. (#8945)
if ( !CKEDITOR.env.hc )
attributes.src = CKEDITOR.getUrl( plugin.path + 'images/spacer.gif' );
if ( realElementType )
attributes[ 'data-cke-real-element-type' ] = realElementType;
if ( isResizable ) {
attributes[ 'data-cke-resizable' ] = isResizable;
var fakeStyle = new cssStyle();
var width = realElement.getAttribute( 'width' ),
height = realElement.getAttribute( 'height' );
width && ( fakeStyle.rules.width = cssLength( width ) );
height && ( fakeStyle.rules.height = cssLength( height ) );
fakeStyle.populate( attributes );
}
return this.document.createElement( 'img', { attributes: attributes } );
};
/**
* @member CKEDITOR.editor
* @todo
*/
CKEDITOR.editor.prototype.createFakeParserElement = function( realElement, className, realElementType, isResizable ) {
var lang = this.lang.fakeobjects,
label = lang[ realElementType ] || lang.unknown,
html;
var writer = new CKEDITOR.htmlParser.basicWriter();
realElement.writeHtml( writer );
html = writer.getHtml();
var attributes = {
'class': className,
'data-cke-realelement': encodeURIComponent( html ),
'data-cke-real-node-type': realElement.type,
alt: label,
title: label,
align: realElement.attributes.align || ''
};
// Do not set "src" on high-contrast so the alt text is displayed. (#8945)
if ( !CKEDITOR.env.hc )
attributes.src = CKEDITOR.getUrl( plugin.path + 'images/spacer.gif' );
if ( realElementType )
attributes[ 'data-cke-real-element-type' ] = realElementType;
if ( isResizable ) {
attributes[ 'data-cke-resizable' ] = isResizable;
var realAttrs = realElement.attributes,
fakeStyle = new cssStyle();
var width = realAttrs.width,
height = realAttrs.height;
width != undefined && ( fakeStyle.rules.width = cssLength( width ) );
height != undefined && ( fakeStyle.rules.height = cssLength( height ) );
fakeStyle.populate( attributes );
}
return new CKEDITOR.htmlParser.element( 'img', attributes );
};
/**
* @member CKEDITOR.editor
* @todo
*/
CKEDITOR.editor.prototype.restoreRealElement = function( fakeElement ) {
if ( fakeElement.data( 'cke-real-node-type' ) != CKEDITOR.NODE_ELEMENT )
return null;
var element = CKEDITOR.dom.element.createFromHtml( decodeURIComponent( fakeElement.data( 'cke-realelement' ) ), this.document );
if ( fakeElement.data( 'cke-resizable' ) ) {
var width = fakeElement.getStyle( 'width' ),
height = fakeElement.getStyle( 'height' );
width && element.setAttribute( 'width', replaceCssLength( element.getAttribute( 'width' ), width ) );
height && element.setAttribute( 'height', replaceCssLength( element.getAttribute( 'height' ), height ) );
}
return element;
};
} )();

View File

@ -0,0 +1,379 @@
/**
* @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
( function() {
var floatSpaceTpl = CKEDITOR.addTemplate( 'floatcontainer', '<div' +
' id="cke_{name}"' +
' class="cke {id} cke_reset_all cke_chrome cke_editor_{name} cke_float cke_{langDir} ' + CKEDITOR.env.cssClass + '"' +
' dir="{langDir}"' +
' title="' + ( CKEDITOR.env.gecko ? ' ' : '' ) + '"' +
' lang="{langCode}"' +
' role="application"' +
' style="{style}"' +
' aria-labelledby="cke_{name}_arialbl"' +
'>' +
'<span id="cke_{name}_arialbl" class="cke_voice_label">{voiceLabel}</span>' +
'<div class="cke_inner">' +
'<div id="{topId}" class="cke_top" role="presentation">{content}</div>' +
'</div>' +
'</div>' ),
win = CKEDITOR.document.getWindow(),
pixelate = CKEDITOR.tools.cssLength;
CKEDITOR.plugins.add( 'floatingspace', {
init: function( editor ) {
// Add listener with lower priority than that in themedui creator.
// Thereby floatingspace will be created only if themedui wasn't used.
editor.on( 'loaded', function() {
attach( this );
}, null, null, 20 );
}
} );
function scrollOffset( side ) {
var pageOffset = side == 'left' ? 'pageXOffset' : 'pageYOffset',
docScrollOffset = side == 'left' ? 'scrollLeft' : 'scrollTop';
return ( pageOffset in win.$ ) ?
win.$[ pageOffset ]
:
CKEDITOR.document.$.documentElement[ docScrollOffset ];
}
function attach( editor ) {
var config = editor.config,
// Get the HTML for the predefined spaces.
topHtml = editor.fire( 'uiSpace', { space: 'top', html: '' } ).html,
// Re-positioning of the space.
layout = ( function() {
// Mode indicates the vertical aligning mode.
var mode, editable,
spaceRect, editorRect, viewRect, spaceHeight, pageScrollX,
// Allow minor adjustments of the float space from custom configs.
dockedOffsetX = config.floatSpaceDockedOffsetX || 0,
dockedOffsetY = config.floatSpaceDockedOffsetY || 0,
pinnedOffsetX = config.floatSpacePinnedOffsetX || 0,
pinnedOffsetY = config.floatSpacePinnedOffsetY || 0;
// Update the float space position.
function updatePos( pos, prop, val ) {
floatSpace.setStyle( prop, pixelate( val ) );
floatSpace.setStyle( 'position', pos );
}
// Change the current mode and update float space position accordingly.
function changeMode( newMode ) {
var editorPos = editable.getDocumentPosition();
switch ( newMode ) {
case 'top':
updatePos( 'absolute', 'top', editorPos.y - spaceHeight - dockedOffsetY );
break;
case 'pin':
updatePos( 'fixed', 'top', pinnedOffsetY );
break;
case 'bottom':
updatePos( 'absolute', 'top', editorPos.y + ( editorRect.height || editorRect.bottom - editorRect.top ) + dockedOffsetY );
break;
}
mode = newMode;
}
return function( evt ) {
// #10112 Do not fail on editable-less editor.
if ( !( editable = editor.editable() ) )
return;
// Show up the space on focus gain.
evt && evt.name == 'focus' && floatSpace.show();
// Reset the horizontal position for below measurement.
floatSpace.removeStyle( 'left' );
floatSpace.removeStyle( 'right' );
// Compute the screen position from the TextRectangle object would
// be very simple, even though the "width"/"height" property is not
// available for all, it's safe to figure that out from the rest.
// http://help.dottoro.com/ljgupwlp.php
spaceRect = floatSpace.getClientRect();
editorRect = editable.getClientRect();
viewRect = win.getViewPaneSize();
spaceHeight = spaceRect.height;
pageScrollX = scrollOffset( 'left' );
// We initialize it as pin mode.
if ( !mode ) {
mode = 'pin';
changeMode( 'pin' );
// Call for a refresh to the actual layout.
layout( evt );
return;
}
// +------------------------ Viewport -+ \
// | | |-> floatSpaceDockedOffsetY
// | ................................. | /
// | |
// | +------ Space -+ |
// | | | |
// | +--------------+ |
// | +------------------ Editor -+ |
// | | | |
//
if ( spaceHeight + dockedOffsetY <= editorRect.top )
changeMode( 'top' );
// +- - - - - - - - - Editor -+
// | |
// +------------------------ Viewport -+ \
// | | | | |-> floatSpacePinnedOffsetY
// | ................................. | /
// | +------ Space -+ | |
// | | | | |
// | +--------------+ | |
// | | | |
// | +---------------------------+ |
// +-----------------------------------+
//
else if ( spaceHeight + dockedOffsetY > viewRect.height - editorRect.bottom )
changeMode( 'pin' );
// +- - - - - - - - - Editor -+
// | |
// +------------------------ Viewport -+ \
// | | | | |-> floatSpacePinnedOffsetY
// | ................................. | /
// | | | |
// | | | |
// | +---------------------------+ |
// | +------ Space -+ |
// | | | |
// | +--------------+ |
//
else
changeMode( 'bottom' );
var mid = viewRect.width / 2,
alignSide =
( editorRect.left > 0 && editorRect.right < viewRect.width && editorRect.width > spaceRect.width ) ?
( editor.config.contentsLangDirection == 'rtl' ? 'right' : 'left' )
:
( mid - editorRect.left > editorRect.right - mid ? 'left' : 'right' ),
offset;
// (#9769) If viewport width is less than space width,
// make sure space never cross the left boundary of the viewport.
// In other words: top-left corner of the space is always visible.
if ( spaceRect.width > viewRect.width ) {
alignSide = 'left';
offset = 0;
}
else {
if ( alignSide == 'left' ) {
// If the space rect fits into viewport, align it
// to the left edge of editor:
//
// +------------------------ Viewport -+
// | |
// | +------------- Space -+ |
// | | | |
// | +---------------------+ |
// | +------------------ Editor -+ |
// | | | |
//
if ( editorRect.left > 0 )
offset = editorRect.left;
// If the left part of the editor is cut off by the left
// edge of the viewport, stick the space to the viewport:
//
// +------------------------ Viewport -+
// | |
// +---------------- Space -+ |
// | | |
// +------------------------+ |
// +----|------------- Editor -+ |
// | | | |
//
else
offset = 0;
}
else {
// If the space rect fits into viewport, align it
// to the right edge of editor:
//
// +------------------------ Viewport -+
// | |
// | +------------- Space -+ |
// | | | |
// | +---------------------+ |
// | +------------------ Editor -+ |
// | | | |
//
if ( editorRect.right < viewRect.width )
offset = viewRect.width - editorRect.right;
// If the right part of the editor is cut off by the right
// edge of the viewport, stick the space to the viewport:
//
// +------------------------ Viewport -+
// | |
// | +------------- Space -+
// | | |
// | +---------------------+
// | +-----------------|- Editor -+
// | | | |
//
else
offset = 0;
}
// (#9769) Finally, stick the space to the opposite side of
// the viewport when it's cut off horizontally on the left/right
// side like below.
//
// This trick reveals cut off space in some edge cases and
// hence it improves accessibility.
//
// +------------------------ Viewport -+
// | |
// | +--------------------|-- Space -+
// | | | |
// | +--------------------|----------+
// | +------- Editor -+ |
// | | | |
//
// becomes:
//
// +------------------------ Viewport -+
// | |
// | +----------------------- Space -+
// | | |
// | +-------------------------------+
// | +------- Editor -+ |
// | | | |
//
if ( offset + spaceRect.width > viewRect.width ) {
alignSide = alignSide == 'left' ? 'right' : 'left';
offset = 0;
}
}
// Pin mode is fixed, so don't include scroll-x.
// (#9903) For mode is "top" or "bottom", add opposite scroll-x for right-aligned space.
var scroll = mode == 'pin' ?
0
:
alignSide == 'left' ? pageScrollX : -pageScrollX;
floatSpace.setStyle( alignSide, pixelate( ( mode == 'pin' ? pinnedOffsetX : dockedOffsetX ) + offset + scroll ) );
};
} )();
if ( topHtml ) {
var floatSpace = CKEDITOR.document.getBody().append( CKEDITOR.dom.element.createFromHtml( floatSpaceTpl.output( {
content: topHtml,
id: editor.id,
langDir: editor.lang.dir,
langCode: editor.langCode,
name: editor.name,
style: 'display:none;z-index:' + ( config.baseFloatZIndex - 1 ),
topId: editor.ui.spaceId( 'top' ),
voiceLabel: editor.lang.editorPanel + ', ' + editor.name
} ) ) ),
// Use event buffers to reduce CPU load when tons of events are fired.
changeBuffer = CKEDITOR.tools.eventsBuffer( 500, layout ),
uiBuffer = CKEDITOR.tools.eventsBuffer( 100, layout );
// There's no need for the floatSpace to be selectable.
floatSpace.unselectable();
// Prevent clicking on non-buttons area of the space from blurring editor.
floatSpace.on( 'mousedown', function( evt ) {
evt = evt.data;
if ( !evt.getTarget().hasAscendant( 'a', 1 ) )
evt.preventDefault();
} );
editor.on( 'focus', function( evt ) {
layout( evt );
editor.on( 'change', changeBuffer.input );
win.on( 'scroll', uiBuffer.input );
win.on( 'resize', uiBuffer.input );
} );
editor.on( 'blur', function() {
floatSpace.hide();
editor.removeListener( 'change', changeBuffer.input );
win.removeListener( 'scroll', uiBuffer.input );
win.removeListener( 'resize', uiBuffer.input );
} );
editor.on( 'destroy', function() {
win.removeListener( 'scroll', uiBuffer.input );
win.removeListener( 'resize', uiBuffer.input );
floatSpace.clearCustomData();
floatSpace.remove();
} );
// Handle initial focus.
if ( editor.focusManager.hasFocus )
floatSpace.show();
// Register this UI space to the focus manager.
editor.focusManager.add( floatSpace, 1 );
}
}
} )();
/**
* Along with {@link #floatSpaceDockedOffsetY} it defines the
* amount of offset (in pixels) between float space and the editable left/right
* boundaries when space element is docked at either side of the editable.
*
* config.floatSpaceDockedOffsetX = 10;
*
* @cfg {Number} [floatSpaceDockedOffsetX=0]
* @member CKEDITOR.config
*/
/**
* Along with {@link #floatSpaceDockedOffsetX} it defines the
* amount of offset (in pixels) between float space and the editable top/bottom
* boundaries when space element is docked at either side of the editable.
*
* config.floatSpaceDockedOffsetY = 10;
*
* @cfg {Number} [floatSpaceDockedOffsetY=0]
* @member CKEDITOR.config
*/
/**
* Along with {@link #floatSpacePinnedOffsetY} it defines the
* amount of offset (in pixels) between float space and the view port boundaries
* when space element is pinned.
*
* config.floatSpacePinnedOffsetX = 20;
*
* @cfg {Number} [floatSpacePinnedOffsetX=0]
* @member CKEDITOR.config
*/
/**
* Along with {@link #floatSpacePinnedOffsetX} it defines the
* amount of offset (in pixels) between float space and the view port boundaries
* when space element is pinned.
*
* config.floatSpacePinnedOffsetY = 20;
*
* @cfg {Number} [floatSpacePinnedOffsetY=0]
* @member CKEDITOR.config
*/

View File

@ -0,0 +1,548 @@
/**
* @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
CKEDITOR.plugins.add( 'floatpanel', {
requires: 'panel'
} );
( function() {
var panels = {};
function getPanel( editor, doc, parentElement, definition, level ) {
// Generates the panel key: docId-eleId-skinName-langDir[-uiColor][-CSSs][-level]
var key = CKEDITOR.tools.genKey( doc.getUniqueId(), parentElement.getUniqueId(), editor.lang.dir, editor.uiColor || '', definition.css || '', level || '' ),
panel = panels[ key ];
if ( !panel ) {
panel = panels[ key ] = new CKEDITOR.ui.panel( doc, definition );
panel.element = parentElement.append( CKEDITOR.dom.element.createFromHtml( panel.render( editor ), doc ) );
panel.element.setStyles( {
display: 'none',
position: 'absolute'
} );
}
return panel;
}
/**
* Represents a floating panel UI element.
*
* It's reused by rich combos, color combos, menus, etc.
* and it renders its content using {@link CKEDITOR.ui.panel}.
*
* @class
* @todo
*/
CKEDITOR.ui.floatPanel = CKEDITOR.tools.createClass( {
/**
* Creates a floatPanel class instance.
*
* @constructor
* @param {CKEDITOR.editor} editor
* @param {CKEDITOR.dom.element} parentElement
* @param {Object} definition Definition of the panel that will be floating.
* @param {Number} level
*/
$: function( editor, parentElement, definition, level ) {
definition.forceIFrame = 1;
// In case of editor with floating toolbar append panels that should float
// to the main UI element.
if ( definition.toolbarRelated && editor.elementMode == CKEDITOR.ELEMENT_MODE_INLINE )
parentElement = CKEDITOR.document.getById( 'cke_' + editor.name );
var doc = parentElement.getDocument(),
panel = getPanel( editor, doc, parentElement, definition, level || 0 ),
element = panel.element,
iframe = element.getFirst(),
that = this;
// Disable native browser menu. (#4825)
element.disableContextMenu();
this.element = element;
this._ = {
editor: editor,
// The panel that will be floating.
panel: panel,
parentElement: parentElement,
definition: definition,
document: doc,
iframe: iframe,
children: [],
dir: editor.lang.dir
};
editor.on( 'mode', hide );
editor.on( 'resize', hide );
// Window resize doesn't cause hide on blur. (#9800)
doc.getWindow().on( 'resize', hide );
// We need a wrapper because events implementation doesn't allow to attach
// one listener more than once for the same event on the same object.
// Remember that floatPanel#hide is shared between all instances.
function hide() {
that.hide();
}
},
proto: {
/**
* @todo
*/
addBlock: function( name, block ) {
return this._.panel.addBlock( name, block );
},
/**
* @todo
*/
addListBlock: function( name, multiSelect ) {
return this._.panel.addListBlock( name, multiSelect );
},
/**
* @todo
*/
getBlock: function( name ) {
return this._.panel.getBlock( name );
},
/**
* Shows panel block.
*
* @param {String} name
* @param {CKEDITOR.dom.element} offsetParent Positioned parent.
* @param {Number} corner
*
* * For LTR (left to right) oriented editor:
* * `1` = top-left
* * `2` = top-right
* * `3` = bottom-right
* * `4` = bottom-left
* * For RTL (right to left):
* * `1` = top-right
* * `2` = top-left
* * `3` = bottom-left
* * `4` = bottom-right
*
* @param {Number} [offsetX=0]
* @param {Number} [offsetY=0]
* @param {Function} [callback] A callback function executed when block positioning is done.
* @todo what do exactly these params mean (especially corner)?
*/
showBlock: function( name, offsetParent, corner, offsetX, offsetY, callback ) {
var panel = this._.panel,
block = panel.showBlock( name );
this.allowBlur( false );
// Record from where the focus is when open panel.
var editable = this._.editor.editable();
this._.returnFocus = editable.hasFocus ? editable : new CKEDITOR.dom.element( CKEDITOR.document.$.activeElement );
var element = this.element,
iframe = this._.iframe,
// Non IE prefer the event into a window object.
focused = CKEDITOR.env.ie ? iframe : new CKEDITOR.dom.window( iframe.$.contentWindow ),
doc = element.getDocument(),
positionedAncestor = this._.parentElement.getPositionedAncestor(),
position = offsetParent.getDocumentPosition( doc ),
positionedAncestorPosition = positionedAncestor ? positionedAncestor.getDocumentPosition( doc ) : { x: 0, y: 0 },
rtl = this._.dir == 'rtl',
left = position.x + ( offsetX || 0 ) - positionedAncestorPosition.x,
top = position.y + ( offsetY || 0 ) - positionedAncestorPosition.y;
// Floating panels are off by (-1px, 0px) in RTL mode. (#3438)
if ( rtl && ( corner == 1 || corner == 4 ) )
left += offsetParent.$.offsetWidth;
else if ( !rtl && ( corner == 2 || corner == 3 ) )
left += offsetParent.$.offsetWidth - 1;
if ( corner == 3 || corner == 4 )
top += offsetParent.$.offsetHeight - 1;
// Memorize offsetParent by it's ID.
this._.panel._.offsetParentId = offsetParent.getId();
element.setStyles( {
top: top + 'px',
left: 0,
display: ''
} );
// Don't use display or visibility style because we need to
// calculate the rendering layout later and focus the element.
element.setOpacity( 0 );
// To allow the context menu to decrease back their width
element.getFirst().removeStyle( 'width' );
// Report to focus manager.
this._.editor.focusManager.add( focused );
// Configure the IFrame blur event. Do that only once.
if ( !this._.blurSet ) {
// With addEventListener compatible browsers, we must
// useCapture when registering the focus/blur events to
// guarantee they will be firing in all situations. (#3068, #3222 )
CKEDITOR.event.useCapture = true;
focused.on( 'blur', function( ev ) {
// As we are using capture to register the listener,
// the blur event may get fired even when focusing
// inside the window itself, so we must ensure the
// target is out of it.
if ( !this.allowBlur() || ev.data.getPhase() != CKEDITOR.EVENT_PHASE_AT_TARGET )
return;
if ( this.visible && !this._.activeChild ) {
// Panel close is caused by user's navigating away the focus, e.g. click outside the panel.
// DO NOT restore focus in this case.
delete this._.returnFocus;
this.hide();
}
}, this );
focused.on( 'focus', function() {
this._.focused = true;
this.hideChild();
this.allowBlur( true );
}, this );
CKEDITOR.event.useCapture = false;
this._.blurSet = 1;
}
panel.onEscape = CKEDITOR.tools.bind( function( keystroke ) {
if ( this.onEscape && this.onEscape( keystroke ) === false )
return false;
}, this );
CKEDITOR.tools.setTimeout( function() {
var panelLoad = CKEDITOR.tools.bind( function() {
var target = element;
// Reset panel width as the new content can be narrower
// than the old one. (#9355)
target.removeStyle( 'width' );
if ( block.autoSize ) {
var panelDoc = block.element.getDocument();
var width = ( CKEDITOR.env.webkit? block.element : panelDoc.getBody() )[ '$' ].scrollWidth;
// Account for extra height needed due to IE quirks box model bug:
// http://en.wikipedia.org/wiki/Internet_Explorer_box_model_bug
// (#3426)
if ( CKEDITOR.env.ie && CKEDITOR.env.quirks && width > 0 )
width += ( target.$.offsetWidth || 0 ) - ( target.$.clientWidth || 0 ) + 3;
// Add some extra pixels to improve the appearance.
width += 10;
target.setStyle( 'width', width + 'px' );
var height = block.element.$.scrollHeight;
// Account for extra height needed due to IE quirks box model bug:
// http://en.wikipedia.org/wiki/Internet_Explorer_box_model_bug
// (#3426)
if ( CKEDITOR.env.ie && CKEDITOR.env.quirks && height > 0 )
height += ( target.$.offsetHeight || 0 ) - ( target.$.clientHeight || 0 ) + 3;
target.setStyle( 'height', height + 'px' );
// Fix IE < 8 visibility.
panel._.currentBlock.element.setStyle( 'display', 'none' ).removeStyle( 'display' );
} else
target.removeStyle( 'height' );
// Flip panel layout horizontally in RTL with known width.
if ( rtl )
left -= element.$.offsetWidth;
// Pop the style now for measurement.
element.setStyle( 'left', left + 'px' );
/* panel layout smartly fit the viewport size. */
var panelElement = panel.element,
panelWindow = panelElement.getWindow(),
rect = element.$.getBoundingClientRect(),
viewportSize = panelWindow.getViewPaneSize();
// Compensation for browsers that dont support "width" and "height".
var rectWidth = rect.width || rect.right - rect.left,
rectHeight = rect.height || rect.bottom - rect.top;
// Check if default horizontal layout is impossible.
var spaceAfter = rtl ? rect.right : viewportSize.width - rect.left,
spaceBefore = rtl ? viewportSize.width - rect.right : rect.left;
if ( rtl ) {
if ( spaceAfter < rectWidth ) {
// Flip to show on right.
if ( spaceBefore > rectWidth )
left += rectWidth;
// Align to window left.
else if ( viewportSize.width > rectWidth )
left = left - rect.left;
// Align to window right, never cutting the panel at right.
else
left = left - rect.right + viewportSize.width;
}
} else if ( spaceAfter < rectWidth ) {
// Flip to show on left.
if ( spaceBefore > rectWidth )
left -= rectWidth;
// Align to window right.
else if ( viewportSize.width > rectWidth )
left = left - rect.right + viewportSize.width;
// Align to window left, never cutting the panel at left.
else
left = left - rect.left;
}
// Check if the default vertical layout is possible.
var spaceBelow = viewportSize.height - rect.top,
spaceAbove = rect.top;
if ( spaceBelow < rectHeight ) {
// Flip to show above.
if ( spaceAbove > rectHeight )
top -= rectHeight;
// Align to window bottom.
else if ( viewportSize.height > rectHeight )
top = top - rect.bottom + viewportSize.height;
// Align to top, never cutting the panel at top.
else
top = top - rect.top;
}
// If IE is in RTL, we have troubles with absolute
// position and horizontal scrolls. Here we have a
// series of hacks to workaround it. (#6146)
if ( CKEDITOR.env.ie ) {
var offsetParent = new CKEDITOR.dom.element( element.$.offsetParent ),
scrollParent = offsetParent;
// Quirks returns <body>, but standards returns <html>.
if ( scrollParent.getName() == 'html' )
scrollParent = scrollParent.getDocument().getBody();
if ( scrollParent.getComputedStyle( 'direction' ) == 'rtl' ) {
// For IE8, there is not much logic on this, but it works.
if ( CKEDITOR.env.ie8Compat )
left -= element.getDocument().getDocumentElement().$.scrollLeft * 2;
else
left -= ( offsetParent.$.scrollWidth - offsetParent.$.clientWidth );
}
}
// Trigger the onHide event of the previously active panel to prevent
// incorrect styles from being applied (#6170)
var innerElement = element.getFirst(),
activePanel;
if ( ( activePanel = innerElement.getCustomData( 'activePanel' ) ) )
activePanel.onHide && activePanel.onHide.call( this, 1 );
innerElement.setCustomData( 'activePanel', this );
element.setStyles( {
top: top + 'px',
left: left + 'px'
} );
element.setOpacity( 1 );
callback && callback();
}, this );
panel.isLoaded ? panelLoad() : panel.onLoad = panelLoad;
CKEDITOR.tools.setTimeout( function() {
var scrollTop = CKEDITOR.env.webkit && CKEDITOR.document.getWindow().getScrollPosition().y;
// Focus the panel frame first, so blur gets fired.
this.focus();
// Focus the block now.
block.element.focus();
// #10623, #10951 - restore the viewport's scroll position after focusing list element.
if ( CKEDITOR.env.webkit )
CKEDITOR.document.getBody().$.scrollTop = scrollTop;
// We need this get fired manually because of unfired focus() function.
this.allowBlur( true );
this._.editor.fire( 'panelShow', this );
}, 0, this );
}, CKEDITOR.env.air ? 200 : 0, this );
this.visible = 1;
if ( this.onShow )
this.onShow.call( this );
},
/**
* Restores last focused element or simply focus panel window.
*/
focus: function() {
// Webkit requires to blur any previous focused page element, in
// order to properly fire the "focus" event.
if ( CKEDITOR.env.webkit ) {
var active = CKEDITOR.document.getActive();
!active.equals( this._.iframe ) && active.$.blur();
}
// Restore last focused element or simply focus panel window.
var focus = this._.lastFocused || this._.iframe.getFrameDocument().getWindow();
focus.focus();
},
/**
* @todo
*/
blur: function() {
var doc = this._.iframe.getFrameDocument(),
active = doc.getActive();
active.is( 'a' ) && ( this._.lastFocused = active );
},
/**
* Hides panel.
*
* @todo
*/
hide: function( returnFocus ) {
if ( this.visible && ( !this.onHide || this.onHide.call( this ) !== true ) ) {
this.hideChild();
// Blur previously focused element. (#6671)
CKEDITOR.env.gecko && this._.iframe.getFrameDocument().$.activeElement.blur();
this.element.setStyle( 'display', 'none' );
this.visible = 0;
this.element.getFirst().removeCustomData( 'activePanel' );
// Return focus properly. (#6247)
var focusReturn = returnFocus && this._.returnFocus;
if ( focusReturn ) {
// Webkit requires focus moved out panel iframe first.
if ( CKEDITOR.env.webkit && focusReturn.type )
focusReturn.getWindow().$.focus();
focusReturn.focus();
}
delete this._.lastFocused;
this._.editor.fire( 'panelHide', this );
}
},
/**
* @todo
*/
allowBlur: function( allow ) // Prevent editor from hiding the panel. #3222.
{
var panel = this._.panel;
if ( allow != undefined )
panel.allowBlur = allow;
return panel.allowBlur;
},
/**
* Shows specified panel as a child of one block of this one.
*
* @param {CKEDITOR.ui.floatPanel} panel
* @param {String} blockName
* @param {CKEDITOR.dom.element} offsetParent Positioned parent.
* @param {Number} corner
*
* * For LTR (left to right) oriented editor:
* * `1` = top-left
* * `2` = top-right
* * `3` = bottom-right
* * `4` = bottom-left
* * For RTL (right to left):
* * `1` = top-right
* * `2` = top-left
* * `3` = bottom-left
* * `4` = bottom-right
*
* @param {Number} [offsetX=0]
* @param {Number} [offsetY=0]
* @todo
*/
showAsChild: function( panel, blockName, offsetParent, corner, offsetX, offsetY ) {
// Skip reshowing of child which is already visible.
if ( this._.activeChild == panel && panel._.panel._.offsetParentId == offsetParent.getId() )
return;
this.hideChild();
panel.onHide = CKEDITOR.tools.bind( function() {
// Use a timeout, so we give time for this menu to get
// potentially focused.
CKEDITOR.tools.setTimeout( function() {
if ( !this._.focused )
this.hide();
}, 0, this );
}, this );
this._.activeChild = panel;
this._.focused = false;
panel.showBlock( blockName, offsetParent, corner, offsetX, offsetY );
this.blur();
/* #3767 IE: Second level menu may not have borders */
if ( CKEDITOR.env.ie7Compat || CKEDITOR.env.ie6Compat ) {
setTimeout( function() {
panel.element.getChild( 0 ).$.style.cssText += '';
}, 100 );
}
},
/**
* @todo
*/
hideChild: function( restoreFocus ) {
var activeChild = this._.activeChild;
if ( activeChild ) {
delete activeChild.onHide;
delete this._.activeChild;
activeChild.hide();
// At this point focus should be moved back to parent panel.
restoreFocus && this.focus();
}
}
}
} );
CKEDITOR.on( 'instanceDestroyed', function() {
var isLastInstance = CKEDITOR.tools.isEmpty( CKEDITOR.instances );
for ( var i in panels ) {
var panel = panels[ i ];
// Safe to destroy it since there're no more instances.(#4241)
if ( isLastInstance )
panel.destroy();
// Panel might be used by other instances, just hide them.(#4552)
else
panel.element.hide();
}
// Remove the registration.
isLastInstance && ( panels = {} );
} );
} )();

View File

@ -0,0 +1,230 @@
/**
* @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
( function() {
function addCombo( editor, comboName, styleType, lang, entries, defaultLabel, styleDefinition, order ) {
var config = editor.config,
style = new CKEDITOR.style( styleDefinition );
// Gets the list of fonts from the settings.
var names = entries.split( ';' ),
values = [];
// Create style objects for all fonts.
var styles = {};
for ( var i = 0; i < names.length; i++ ) {
var parts = names[ i ];
if ( parts ) {
parts = parts.split( '/' );
var vars = {},
name = names[ i ] = parts[ 0 ];
vars[ styleType ] = values[ i ] = parts[ 1 ] || name;
styles[ name ] = new CKEDITOR.style( styleDefinition, vars );
styles[ name ]._.definition.name = name;
} else
names.splice( i--, 1 );
}
editor.ui.addRichCombo( comboName, {
label: lang.label,
title: lang.panelTitle,
toolbar: 'styles,' + order,
allowedContent: style,
requiredContent: style,
panel: {
css: [ CKEDITOR.skin.getPath( 'editor' ) ].concat( config.contentsCss ),
multiSelect: false,
attributes: { 'aria-label': lang.panelTitle }
},
init: function() {
this.startGroup( lang.panelTitle );
for ( var i = 0; i < names.length; i++ ) {
var name = names[ i ];
// Add the tag entry to the panel list.
this.add( name, styles[ name ].buildPreview(), name );
}
},
onClick: function( value ) {
editor.focus();
editor.fire( 'saveSnapshot' );
var style = styles[ value ];
editor[ this.getValue() == value ? 'removeStyle' : 'applyStyle' ]( style );
editor.fire( 'saveSnapshot' );
},
onRender: function() {
editor.on( 'selectionChange', function( ev ) {
var currentValue = this.getValue();
var elementPath = ev.data.path,
elements = elementPath.elements;
// For each element into the elements path.
for ( var i = 0, element; i < elements.length; i++ ) {
element = elements[ i ];
// Check if the element is removable by any of
// the styles.
for ( var value in styles ) {
if ( styles[ value ].checkElementMatch( element, true ) ) {
if ( value != currentValue )
this.setValue( value );
return;
}
}
}
// If no styles match, just empty it.
this.setValue( '', defaultLabel );
}, this );
},
refresh: function() {
if ( !editor.activeFilter.check( style ) )
this.setState( CKEDITOR.TRISTATE_DISABLED );
}
} );
}
CKEDITOR.plugins.add( 'font', {
requires: 'richcombo',
lang: 'af,ar,bg,bn,bs,ca,cs,cy,da,de,el,en,en-au,en-ca,en-gb,eo,es,et,eu,fa,fi,fo,fr,fr-ca,gl,gu,he,hi,hr,hu,id,is,it,ja,ka,km,ko,ku,lt,lv,mk,mn,ms,nb,nl,no,pl,pt,pt-br,ro,ru,si,sk,sl,sq,sr,sr-latn,sv,th,tr,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE%
init: function( editor ) {
var config = editor.config;
addCombo( editor, 'Font', 'family', editor.lang.font, config.font_names, config.font_defaultLabel, config.font_style, 30 );
addCombo( editor, 'FontSize', 'size', editor.lang.font.fontSize, config.fontSize_sizes, config.fontSize_defaultLabel, config.fontSize_style, 40 );
}
} );
} )();
/**
* The list of fonts names to be displayed in the Font combo in the toolbar.
* Entries are separated by semi-colons (`';'`), while it's possible to have more
* than one font for each entry, in the HTML way (separated by comma).
*
* A display name may be optionally defined by prefixing the entries with the
* name and the slash character. For example, `'Arial/Arial, Helvetica, sans-serif'`
* will be displayed as `'Arial'` in the list, but will be outputted as
* `'Arial, Helvetica, sans-serif'`.
*
* config.font_names =
* 'Arial/Arial, Helvetica, sans-serif;' +
* 'Times New Roman/Times New Roman, Times, serif;' +
* 'Verdana';
*
* config.font_names = 'Arial;Times New Roman;Verdana';
*
* @cfg {String} [font_names=see source]
* @member CKEDITOR.config
*/
CKEDITOR.config.font_names = 'Arial/Arial, Helvetica, sans-serif;' +
'Comic Sans MS/Comic Sans MS, cursive;' +
'Courier New/Courier New, Courier, monospace;' +
'Georgia/Georgia, serif;' +
'Lucida Sans Unicode/Lucida Sans Unicode, Lucida Grande, sans-serif;' +
'Tahoma/Tahoma, Geneva, sans-serif;' +
'Times New Roman/Times New Roman, Times, serif;' +
'Trebuchet MS/Trebuchet MS, Helvetica, sans-serif;' +
'Verdana/Verdana, Geneva, sans-serif';
/**
* The text to be displayed in the Font combo is none of the available values
* matches the current cursor position or text selection.
*
* // If the default site font is Arial, we may making it more explicit to the end user.
* config.font_defaultLabel = 'Arial';
*
* @cfg {String} [font_defaultLabel='']
* @member CKEDITOR.config
*/
CKEDITOR.config.font_defaultLabel = '';
/**
* The style definition to be used to apply the font in the text.
*
* // This is actually the default value for it.
* config.font_style = {
* element: 'span',
* styles: { 'font-family': '#(family)' },
* overrides: [ { element: 'font', attributes: { 'face': null } } ]
* };
*
* @cfg {Object} [font_style=see example]
* @member CKEDITOR.config
*/
CKEDITOR.config.font_style = {
element: 'span',
styles: { 'font-family': '#(family)' },
overrides: [ {
element: 'font', attributes: { 'face': null }
} ]
};
/**
* The list of fonts size to be displayed in the Font Size combo in the
* toolbar. Entries are separated by semi-colons (`';'`).
*
* Any kind of "CSS like" size can be used, like `'12px'`, `'2.3em'`, `'130%'`,
* `'larger'` or `'x-small'`.
*
* A display name may be optionally defined by prefixing the entries with the
* name and the slash character. For example, `'Bigger Font/14px'` will be
* displayed as `'Bigger Font'` in the list, but will be outputted as `'14px'`.
*
* config.fontSize_sizes = '16/16px;24/24px;48/48px;';
*
* config.fontSize_sizes = '12px;2.3em;130%;larger;x-small';
*
* config.fontSize_sizes = '12 Pixels/12px;Big/2.3em;30 Percent More/130%;Bigger/larger;Very Small/x-small';
*
* @cfg {String} [fontSize_sizes=see source]
* @member CKEDITOR.config
*/
CKEDITOR.config.fontSize_sizes = '8/8px;9/9px;10/10px;11/11px;12/12px;14/14px;16/16px;18/18px;20/20px;22/22px;24/24px;26/26px;28/28px;36/36px;48/48px;72/72px';
/**
* The text to be displayed in the Font Size combo is none of the available
* values matches the current cursor position or text selection.
*
* // If the default site font size is 12px, we may making it more explicit to the end user.
* config.fontSize_defaultLabel = '12px';
*
* @cfg {String} [fontSize_defaultLabel='']
* @member CKEDITOR.config
*/
CKEDITOR.config.fontSize_defaultLabel = '';
/**
* The style definition to be used to apply the font size in the text.
*
* // This is actually the default value for it.
* config.fontSize_style = {
* element: 'span',
* styles: { 'font-size': '#(size)' },
* overrides: [ { element :'font', attributes: { 'size': null } } ]
* };
*
* @cfg {Object} [fontSize_style=see example]
* @member CKEDITOR.config
*/
CKEDITOR.config.fontSize_style = {
element: 'span',
styles: { 'font-size': '#(size)' },
overrides: [ {
element: 'font', attributes: { 'size': null }
} ]
};

View File

@ -0,0 +1,244 @@
/**
* @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
CKEDITOR.plugins.add( 'format', {
requires: 'richcombo',
lang: 'af,ar,bg,bn,bs,ca,cs,cy,da,de,el,en,en-au,en-ca,en-gb,eo,es,et,eu,fa,fi,fo,fr,fr-ca,gl,gu,he,hi,hr,hu,id,is,it,ja,ka,km,ko,ku,lt,lv,mk,mn,ms,nb,nl,no,pl,pt,pt-br,ro,ru,si,sk,sl,sq,sr,sr-latn,sv,th,tr,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE%
init: function( editor ) {
if ( editor.blockless )
return;
var config = editor.config,
lang = editor.lang.format;
// Gets the list of tags from the settings.
var tags = config.format_tags.split( ';' );
// Create style objects for all defined styles.
var styles = {},
stylesCount = 0,
allowedContent = [];
for ( var i = 0; i < tags.length; i++ ) {
var tag = tags[ i ];
var style = new CKEDITOR.style( config[ 'format_' + tag ] );
if ( !editor.filter.customConfig || editor.filter.check( style ) ) {
stylesCount++;
styles[ tag ] = style;
styles[ tag ]._.enterMode = editor.config.enterMode;
allowedContent.push( style );
}
}
// Hide entire combo when all formats are rejected.
if ( stylesCount === 0 )
return;
editor.ui.addRichCombo( 'Format', {
label: lang.label,
title: lang.panelTitle,
toolbar: 'styles,20',
allowedContent: allowedContent,
panel: {
css: [ CKEDITOR.skin.getPath( 'editor' ) ].concat( config.contentsCss ),
multiSelect: false,
attributes: { 'aria-label': lang.panelTitle }
},
init: function() {
this.startGroup( lang.panelTitle );
for ( var tag in styles ) {
var label = lang[ 'tag_' + tag ];
// Add the tag entry to the panel list.
this.add( tag, styles[ tag ].buildPreview( label ), label );
}
},
onClick: function( value ) {
editor.focus();
editor.fire( 'saveSnapshot' );
var style = styles[ value ],
elementPath = editor.elementPath();
editor[ style.checkActive( elementPath ) ? 'removeStyle' : 'applyStyle' ]( style );
// Save the undo snapshot after all changes are affected. (#4899)
setTimeout( function() {
editor.fire( 'saveSnapshot' );
}, 0 );
},
onRender: function() {
editor.on( 'selectionChange', function( ev ) {
var currentTag = this.getValue(),
elementPath = ev.data.path;
this.refresh();
for ( var tag in styles ) {
if ( styles[ tag ].checkActive( elementPath ) ) {
if ( tag != currentTag )
this.setValue( tag, editor.lang.format[ 'tag_' + tag ] );
return;
}
}
// If no styles match, just empty it.
this.setValue( '' );
}, this );
},
onOpen: function() {
this.showAll();
for ( var name in styles ) {
var style = styles[ name ];
// Check if that style is enabled in activeFilter.
if ( !editor.activeFilter.check( style ) )
this.hideItem( name );
}
},
refresh: function() {
var elementPath = editor.elementPath();
if ( !elementPath )
return;
// Check if element path contains 'p' element.
if ( !elementPath.isContextFor( 'p' ) ) {
this.setState( CKEDITOR.TRISTATE_DISABLED );
return;
}
// Check if there is any available style.
for ( var name in styles ) {
if ( editor.activeFilter.check( styles[ name ] ) )
return;
}
this.setState( CKEDITOR.TRISTATE_DISABLED );
}
} );
}
} );
/**
* A list of semi colon separated style names (by default tags) representing
* the style definition for each entry to be displayed in the Format combo in
* the toolbar. Each entry must have its relative definition configuration in a
* setting named `'format_(tagName)'`. For example, the `'p'` entry has its
* definition taken from `config.format_p`.
*
* config.format_tags = 'p;h2;h3;pre';
*
* @cfg {String} [format_tags='p;h1;h2;h3;h4;h5;h6;pre;address;div']
* @member CKEDITOR.config
*/
CKEDITOR.config.format_tags = 'p;h1;h2;h3;h4;h5;h6;pre;address;div';
/**
* The style definition to be used to apply the `'Normal'` format.
*
* config.format_p = { element: 'p', attributes: { 'class': 'normalPara' } };
*
* @cfg {Object} [format_p={ element: 'p' }]
* @member CKEDITOR.config
*/
CKEDITOR.config.format_p = { element: 'p' };
/**
* The style definition to be used to apply the `'Normal (DIV)'` format.
*
* config.format_div = { element: 'div', attributes: { 'class': 'normalDiv' } };
*
* @cfg {Object} [format_div={ element: 'div' }]
* @member CKEDITOR.config
*/
CKEDITOR.config.format_div = { element: 'div' };
/**
* The style definition to be used to apply the `'Formatted'` format.
*
* config.format_pre = { element: 'pre', attributes: { 'class': 'code' } };
*
* @cfg {Object} [format_pre={ element: 'pre' }]
* @member CKEDITOR.config
*/
CKEDITOR.config.format_pre = { element: 'pre' };
/**
* The style definition to be used to apply the `'Address'` format.
*
* config.format_address = { element: 'address', attributes: { 'class': 'styledAddress' } };
*
* @cfg {Object} [format_address={ element: 'address' }]
* @member CKEDITOR.config
*/
CKEDITOR.config.format_address = { element: 'address' };
/**
* The style definition to be used to apply the `'Heading 1'` format.
*
* config.format_h1 = { element: 'h1', attributes: { 'class': 'contentTitle1' } };
*
* @cfg {Object} [format_h1={ element: 'h1' }]
* @member CKEDITOR.config
*/
CKEDITOR.config.format_h1 = { element: 'h1' };
/**
* The style definition to be used to apply the `'Heading 2'` format.
*
* config.format_h2 = { element: 'h2', attributes: { 'class': 'contentTitle2' } };
*
* @cfg {Object} [format_h2={ element: 'h2' }]
* @member CKEDITOR.config
*/
CKEDITOR.config.format_h2 = { element: 'h2' };
/**
* The style definition to be used to apply the `'Heading 3'` format.
*
* config.format_h3 = { element: 'h3', attributes: { 'class': 'contentTitle3' } };
*
* @cfg {Object} [format_h3={ element: 'h3' }]
* @member CKEDITOR.config
*/
CKEDITOR.config.format_h3 = { element: 'h3' };
/**
* The style definition to be used to apply the `'Heading 4'` format.
*
* config.format_h4 = { element: 'h4', attributes: { 'class': 'contentTitle4' } };
*
* @cfg {Object} [format_h4={ element: 'h4' }]
* @member CKEDITOR.config
*/
CKEDITOR.config.format_h4 = { element: 'h4' };
/**
* The style definition to be used to apply the `'Heading 5'` format.
*
* config.format_h5 = { element: 'h5', attributes: { 'class': 'contentTitle5' } };
*
* @cfg {Object} [format_h5={ element: 'h5' }]
* @member CKEDITOR.config
*/
CKEDITOR.config.format_h5 = { element: 'h5' };
/**
* The style definition to be used to apply the `'Heading 6'` format.
*
* config.format_h6 = { element: 'h6', attributes: { 'class': 'contentTitle6' } };
*
* @cfg {Object} [format_h6={ element: 'h6' }]
* @member CKEDITOR.config
*/
CKEDITOR.config.format_h6 = { element: 'h6' };

View File

@ -0,0 +1,41 @@
/**
* @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
/**
* @fileOverview Horizontal Rule plugin.
*/
( function() {
var horizontalruleCmd = {
canUndo: false, // The undo snapshot will be handled by 'insertElement'.
exec: function( editor ) {
var hr = editor.document.createElement( 'hr' );
editor.insertElement( hr );
},
allowedContent: 'hr',
requiredContent: 'hr'
};
var pluginName = 'horizontalrule';
// Register a plugin named "horizontalrule".
CKEDITOR.plugins.add( pluginName, {
lang: 'af,ar,bg,bn,bs,ca,cs,cy,da,de,el,en,en-au,en-ca,en-gb,eo,es,et,eu,fa,fi,fo,fr,fr-ca,gl,gu,he,hi,hr,hu,id,is,it,ja,ka,km,ko,ku,lt,lv,mk,mn,ms,nb,nl,no,pl,pt,pt-br,ro,ru,si,sk,sl,sq,sr,sr-latn,sv,th,tr,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE%
icons: 'horizontalrule', // %REMOVE_LINE_CORE%
hidpi: true, // %REMOVE_LINE_CORE%
init: function( editor ) {
if ( editor.blockless )
return;
editor.addCommand( pluginName, horizontalruleCmd );
editor.ui.addButton && editor.ui.addButton( 'HorizontalRule', {
label: editor.lang.horizontalrule.toolbar,
command: pluginName,
toolbar: 'insert,40'
} );
}
} );
} )();

View File

@ -0,0 +1,359 @@
/**
* @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
CKEDITOR.plugins.add( 'htmlwriter', {
init: function( editor ) {
var writer = new CKEDITOR.htmlWriter();
writer.forceSimpleAmpersand = editor.config.forceSimpleAmpersand;
writer.indentationChars = editor.config.dataIndentationChars || '\t';
// Overwrite default basicWriter initialized in hmtlDataProcessor constructor.
editor.dataProcessor.writer = writer;
}
} );
/**
* Class used to write HTML data.
*
* var writer = new CKEDITOR.htmlWriter();
* writer.openTag( 'p' );
* writer.attribute( 'class', 'MyClass' );
* writer.openTagClose( 'p' );
* writer.text( 'Hello' );
* writer.closeTag( 'p' );
* alert( writer.getHtml() ); // '<p class="MyClass">Hello</p>'
*
* @class
* @extends CKEDITOR.htmlParser.basicWriter
*/
CKEDITOR.htmlWriter = CKEDITOR.tools.createClass( {
base: CKEDITOR.htmlParser.basicWriter,
/**
* Creates a htmlWriter class instance.
*
* @constructor
*/
$: function() {
// Call the base contructor.
this.base();
/**
* The characters to be used for each identation step.
*
* // Use tab for indentation.
* editorInstance.dataProcessor.writer.indentationChars = '\t';
*/
this.indentationChars = '\t';
/**
* The characters to be used to close "self-closing" elements, like `<br>` or `<img>`.
*
* // Use HTML4 notation for self-closing elements.
* editorInstance.dataProcessor.writer.selfClosingEnd = '>';
*/
this.selfClosingEnd = ' />';
/**
* The characters to be used for line breaks.
*
* // Use CRLF for line breaks.
* editorInstance.dataProcessor.writer.lineBreakChars = '\r\n';
*/
this.lineBreakChars = '\n';
this.sortAttributes = 1;
this._.indent = 0;
this._.indentation = '';
// Indicate preformatted block context status. (#5789)
this._.inPre = 0;
this._.rules = {};
var dtd = CKEDITOR.dtd;
for ( var e in CKEDITOR.tools.extend( {}, dtd.$nonBodyContent, dtd.$block, dtd.$listItem, dtd.$tableContent ) ) {
this.setRules( e, {
indent: !dtd[ e ][ '#' ],
breakBeforeOpen: 1,
breakBeforeClose: !dtd[ e ][ '#' ],
breakAfterClose: 1,
needsSpace: ( e in dtd.$block ) && !( e in { li: 1, dt: 1, dd: 1 } )
} );
}
this.setRules( 'br', { breakAfterOpen: 1 } );
this.setRules( 'title', {
indent: 0,
breakAfterOpen: 0
} );
this.setRules( 'style', {
indent: 0,
breakBeforeClose: 1
} );
this.setRules( 'pre', {
breakAfterOpen: 1, // Keep line break after the opening tag
indent: 0 // Disable indentation on <pre>.
} );
},
proto: {
/**
* Writes the tag opening part for a opener tag.
*
* // Writes '<p'.
* writer.openTag( 'p', { class : 'MyClass', id : 'MyId' } );
*
* @param {String} tagName The element name for this tag.
* @param {Object} attributes The attributes defined for this tag. The
* attributes could be used to inspect the tag.
*/
openTag: function( tagName, attributes ) {
var rules = this._.rules[ tagName ];
if ( this._.afterCloser && rules && rules.needsSpace && this._.needsSpace )
this._.output.push( '\n' );
if ( this._.indent )
this.indentation();
// Do not break if indenting.
else if ( rules && rules.breakBeforeOpen ) {
this.lineBreak();
this.indentation();
}
this._.output.push( '<', tagName );
this._.afterCloser = 0;
},
/**
* Writes the tag closing part for a opener tag.
*
* // Writes '>'.
* writer.openTagClose( 'p', false );
*
* // Writes ' />'.
* writer.openTagClose( 'br', true );
*
* @param {String} tagName The element name for this tag.
* @param {Boolean} isSelfClose Indicates that this is a self-closing tag,
* like `<br>` or `<img>`.
*/
openTagClose: function( tagName, isSelfClose ) {
var rules = this._.rules[ tagName ];
if ( isSelfClose ) {
this._.output.push( this.selfClosingEnd );
if ( rules && rules.breakAfterClose )
this._.needsSpace = rules.needsSpace;
} else {
this._.output.push( '>' );
if ( rules && rules.indent )
this._.indentation += this.indentationChars;
}
if ( rules && rules.breakAfterOpen )
this.lineBreak();
tagName == 'pre' && ( this._.inPre = 1 );
},
/**
* Writes an attribute. This function should be called after opening the
* tag with {@link #openTagClose}.
*
* // Writes ' class="MyClass"'.
* writer.attribute( 'class', 'MyClass' );
*
* @param {String} attName The attribute name.
* @param {String} attValue The attribute value.
*/
attribute: function( attName, attValue ) {
if ( typeof attValue == 'string' ) {
this.forceSimpleAmpersand && ( attValue = attValue.replace( /&amp;/g, '&' ) );
// Browsers don't always escape special character in attribute values. (#4683, #4719).
attValue = CKEDITOR.tools.htmlEncodeAttr( attValue );
}
this._.output.push( ' ', attName, '="', attValue, '"' );
},
/**
* Writes a closer tag.
*
* // Writes '</p>'.
* writer.closeTag( 'p' );
*
* @param {String} tagName The element name for this tag.
*/
closeTag: function( tagName ) {
var rules = this._.rules[ tagName ];
if ( rules && rules.indent )
this._.indentation = this._.indentation.substr( this.indentationChars.length );
if ( this._.indent )
this.indentation();
// Do not break if indenting.
else if ( rules && rules.breakBeforeClose ) {
this.lineBreak();
this.indentation();
}
this._.output.push( '</', tagName, '>' );
tagName == 'pre' && ( this._.inPre = 0 );
if ( rules && rules.breakAfterClose ) {
this.lineBreak();
this._.needsSpace = rules.needsSpace;
}
this._.afterCloser = 1;
},
/**
* Writes text.
*
* // Writes 'Hello Word'.
* writer.text( 'Hello Word' );
*
* @param {String} text The text value
*/
text: function( text ) {
if ( this._.indent ) {
this.indentation();
!this._.inPre && ( text = CKEDITOR.tools.ltrim( text ) );
}
this._.output.push( text );
},
/**
* Writes a comment.
*
* // Writes "<!-- My comment -->".
* writer.comment( ' My comment ' );
*
* @param {String} comment The comment text.
*/
comment: function( comment ) {
if ( this._.indent )
this.indentation();
this._.output.push( '<!--', comment, '-->' );
},
/**
* Writes a line break. It uses the {@link #lineBreakChars} property for it.
*
* // Writes '\n' (e.g.).
* writer.lineBreak();
*/
lineBreak: function() {
if ( !this._.inPre && this._.output.length > 0 )
this._.output.push( this.lineBreakChars );
this._.indent = 1;
},
/**
* Writes the current indentation chars. It uses the {@link #indentationChars}
* property, repeating it for the current indentation steps.
*
* // Writes '\t' (e.g.).
* writer.indentation();
*/
indentation: function() {
if ( !this._.inPre && this._.indentation )
this._.output.push( this._.indentation );
this._.indent = 0;
},
/**
* Empties the current output buffer. It also brings back the default
* values of the writer flags.
*
* writer.reset();
*/
reset: function() {
this._.output = [];
this._.indent = 0;
this._.indentation = '';
this._.afterCloser = 0;
this._.inPre = 0;
},
/**
* Sets formatting rules for a give element. The possible rules are:
*
* * `indent`: indent the element contents.
* * `breakBeforeOpen`: break line before the opener tag for this element.
* * `breakAfterOpen`: break line after the opener tag for this element.
* * `breakBeforeClose`: break line before the closer tag for this element.
* * `breakAfterClose`: break line after the closer tag for this element.
*
* All rules default to `false`. Each call to the function overrides
* already present rules, leaving the undefined untouched.
*
* By default, all elements available in the {@link CKEDITOR.dtd#$block},
* {@link CKEDITOR.dtd#$listItem} and {@link CKEDITOR.dtd#$tableContent}
* lists have all the above rules set to `true`. Additionaly, the `<br>`
* element has the `breakAfterOpen` set to `true`.
*
* // Break line before and after "img" tags.
* writer.setRules( 'img', {
* breakBeforeOpen: true
* breakAfterOpen: true
* } );
*
* // Reset the rules for the "h1" tag.
* writer.setRules( 'h1', {} );
*
* @param {String} tagName The element name to which set the rules.
* @param {Object} rules An object containing the element rules.
*/
setRules: function( tagName, rules ) {
var currentRules = this._.rules[ tagName ];
if ( currentRules )
CKEDITOR.tools.extend( currentRules, rules, true );
else
this._.rules[ tagName ] = rules;
}
}
} );
/**
* Whether to force using `'&'` instead of `'&amp;'` in elements attributes
* values, it's not recommended to change this setting for compliance with the
* W3C XHTML 1.0 standards ([C.12, XHTML 1.0](http://www.w3.org/TR/xhtml1/#C_12)).
*
* // Use `'&'` instead of `'&amp;'`
* CKEDITOR.config.forceSimpleAmpersand = true;
*
* @cfg {Boolean} [forceSimpleAmpersand=false]
* @member CKEDITOR.config
*/
/**
* The characters to be used for indenting the HTML produced by the editor.
* Using characters different than `' '` (space) and `'\t'` (tab) is definitely
* a bad idea as it'll mess the code.
*
* // No indentation.
* CKEDITOR.config.dataIndentationChars = '';
*
* // Use two spaces for indentation.
* CKEDITOR.config.dataIndentationChars = ' ';
*
* @cfg {String} [dataIndentationChars='\t']
* @member CKEDITOR.config
*/

View File

@ -0,0 +1,83 @@
/**
* @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
( function() {
CKEDITOR.plugins.add( 'iframe', {
requires: 'dialog,fakeobjects',
lang: 'af,ar,bg,bn,bs,ca,cs,cy,da,de,el,en,en-au,en-ca,en-gb,eo,es,et,eu,fa,fi,fo,fr,fr-ca,gl,gu,he,hi,hr,hu,id,is,it,ja,ka,km,ko,ku,lt,lv,mk,mn,ms,nb,nl,no,pl,pt,pt-br,ro,ru,si,sk,sl,sq,sr,sr-latn,sv,th,tr,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE%
icons: 'iframe', // %REMOVE_LINE_CORE%
hidpi: true, // %REMOVE_LINE_CORE%
onLoad: function() {
CKEDITOR.addCss( 'img.cke_iframe' +
'{' +
'background-image: url(' + CKEDITOR.getUrl( this.path + 'images/placeholder.png' ) + ');' +
'background-position: center center;' +
'background-repeat: no-repeat;' +
'border: 1px solid #a9a9a9;' +
'width: 80px;' +
'height: 80px;' +
'}'
);
},
init: function( editor ) {
var pluginName = 'iframe',
lang = editor.lang.iframe,
allowed = 'iframe[align,longdesc,frameborder,height,name,scrolling,src,title,width]';
if ( editor.plugins.dialogadvtab )
allowed += ';iframe' + editor.plugins.dialogadvtab.allowedContent( { id: 1, classes: 1, styles: 1 } );
CKEDITOR.dialog.add( pluginName, this.path + 'dialogs/iframe.js' );
editor.addCommand( pluginName, new CKEDITOR.dialogCommand( pluginName, {
allowedContent: allowed,
requiredContent: 'iframe'
} ) );
editor.ui.addButton && editor.ui.addButton( 'Iframe', {
label: lang.toolbar,
command: pluginName,
toolbar: 'insert,80'
} );
editor.on( 'doubleclick', function( evt ) {
var element = evt.data.element;
if ( element.is( 'img' ) && element.data( 'cke-real-element-type' ) == 'iframe' )
evt.data.dialog = 'iframe';
} );
if ( editor.addMenuItems ) {
editor.addMenuItems( {
iframe: {
label: lang.title,
command: 'iframe',
group: 'image'
}
} );
}
// If the "contextmenu" plugin is loaded, register the listeners.
if ( editor.contextMenu ) {
editor.contextMenu.addListener( function( element, selection ) {
if ( element && element.is( 'img' ) && element.data( 'cke-real-element-type' ) == 'iframe' )
return { iframe: CKEDITOR.TRISTATE_OFF };
} );
}
},
afterInit: function( editor ) {
var dataProcessor = editor.dataProcessor,
dataFilter = dataProcessor && dataProcessor.dataFilter;
if ( dataFilter ) {
dataFilter.addRules( {
elements: {
iframe: function( element ) {
return editor.createFakeParserElement( element, 'cke_iframe', 'iframe', true );
}
}
} );
}
}
} );
} )();

View File

@ -0,0 +1,170 @@
/**
* @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
/**
* @fileOverview Image plugin
*/
( function() {
CKEDITOR.plugins.add( 'image', {
requires: 'dialog',
lang: 'af,ar,bg,bn,bs,ca,cs,cy,da,de,el,en,en-au,en-ca,en-gb,eo,es,et,eu,fa,fi,fo,fr,fr-ca,gl,gu,he,hi,hr,hu,id,is,it,ja,ka,km,ko,ku,lt,lv,mk,mn,ms,nb,nl,no,pl,pt,pt-br,ro,ru,si,sk,sl,sq,sr,sr-latn,sv,th,tr,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE%
icons: 'image', // %REMOVE_LINE_CORE%
hidpi: true, // %REMOVE_LINE_CORE%
init: function( editor ) {
// Abort when Image2 is to be loaded since both plugins
// share the same button, command, etc. names (#11222).
if ( editor.plugins.image2 )
return;
var pluginName = 'image';
// Register the dialog.
CKEDITOR.dialog.add( pluginName, this.path + 'dialogs/image.js' );
var allowed = 'img[alt,!src]{border-style,border-width,float,height,margin,margin-bottom,margin-left,margin-right,margin-top,width}',
required = 'img[alt,src]';
if ( CKEDITOR.dialog.isTabEnabled( editor, pluginName, 'advanced' ) )
allowed = 'img[alt,dir,id,lang,longdesc,!src,title]{*}(*)';
// Register the command.
editor.addCommand( pluginName, new CKEDITOR.dialogCommand( pluginName, {
allowedContent: allowed,
requiredContent: required,
contentTransformations: [
[ 'img{width}: sizeToStyle', 'img[width]: sizeToAttribute' ],
[ 'img{float}: alignmentToStyle', 'img[align]: alignmentToAttribute' ]
]
} ) );
// Register the toolbar button.
editor.ui.addButton && editor.ui.addButton( 'Image', {
label: editor.lang.common.image,
command: pluginName,
toolbar: 'insert,10'
} );
editor.on( 'doubleclick', function( evt ) {
var element = evt.data.element;
if ( element.is( 'img' ) && !element.data( 'cke-realelement' ) && !element.isReadOnly() )
evt.data.dialog = 'image';
} );
// If the "menu" plugin is loaded, register the menu items.
if ( editor.addMenuItems ) {
editor.addMenuItems( {
image: {
label: editor.lang.image.menu,
command: 'image',
group: 'image'
}
} );
}
// If the "contextmenu" plugin is loaded, register the listeners.
if ( editor.contextMenu ) {
editor.contextMenu.addListener( function( element, selection ) {
if ( getSelectedImage( editor, element ) )
return { image: CKEDITOR.TRISTATE_OFF };
} );
}
},
afterInit: function( editor ) {
// Abort when Image2 is to be loaded since both plugins
// share the same button, command, etc. names (#11222).
if ( editor.plugins.image2 )
return;
// Customize the behavior of the alignment commands. (#7430)
setupAlignCommand( 'left' );
setupAlignCommand( 'right' );
setupAlignCommand( 'center' );
setupAlignCommand( 'block' );
function setupAlignCommand( value ) {
var command = editor.getCommand( 'justify' + value );
if ( command ) {
if ( value == 'left' || value == 'right' ) {
command.on( 'exec', function( evt ) {
var img = getSelectedImage( editor ),
align;
if ( img ) {
align = getImageAlignment( img );
if ( align == value ) {
img.removeStyle( 'float' );
// Remove "align" attribute when necessary.
if ( value == getImageAlignment( img ) )
img.removeAttribute( 'align' );
} else
img.setStyle( 'float', value );
evt.cancel();
}
} );
}
command.on( 'refresh', function( evt ) {
var img = getSelectedImage( editor ),
align;
if ( img ) {
align = getImageAlignment( img );
this.setState(
( align == value ) ? CKEDITOR.TRISTATE_ON : ( value == 'right' || value == 'left' ) ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED );
evt.cancel();
}
} );
}
}
}
} );
function getSelectedImage( editor, element ) {
if ( !element ) {
var sel = editor.getSelection();
element = sel.getSelectedElement();
}
if ( element && element.is( 'img' ) && !element.data( 'cke-realelement' ) && !element.isReadOnly() )
return element;
}
function getImageAlignment( element ) {
var align = element.getStyle( 'float' );
if ( align == 'inherit' || align == 'none' )
align = 0;
if ( !align )
align = element.getAttribute( 'align' );
return align;
}
} )();
/**
* Whether to remove links when emptying the link URL field in the image dialog.
*
* config.image_removeLinkByEmptyURL = false;
*
* @cfg {Boolean} [image_removeLinkByEmptyURL=true]
* @member CKEDITOR.config
*/
CKEDITOR.config.image_removeLinkByEmptyURL = true;
/**
* Padding text to set off the image in preview area.
*
* config.image_previewText = CKEDITOR.tools.repeat( '___ ', 100 );
*
* @cfg {String} [image_previewText='Lorem ipsum dolor...' (placeholder text)]
* @member CKEDITOR.config
*/

View File

@ -0,0 +1,461 @@
/**
* @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
/**
* @fileOverview Increase and Decrease Indent commands.
*/
( function() {
'use strict';
var TRISTATE_DISABLED = CKEDITOR.TRISTATE_DISABLED,
TRISTATE_OFF = CKEDITOR.TRISTATE_OFF;
CKEDITOR.plugins.add( 'indent', {
lang: 'af,ar,bg,bn,bs,ca,cs,cy,da,de,el,en,en-au,en-ca,en-gb,eo,es,et,eu,fa,fi,fo,fr,fr-ca,gl,gu,he,hi,hr,hu,id,is,it,ja,ka,km,ko,ku,lt,lv,mk,mn,ms,nb,nl,no,pl,pt,pt-br,ro,ru,si,sk,sl,sq,sr,sr-latn,sv,th,tr,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE%
icons: 'indent,indent-rtl,outdent,outdent-rtl', // %REMOVE_LINE_CORE%
init: function( editor ) {
var genericDefinition = CKEDITOR.plugins.indent.genericDefinition;
// Register generic commands.
setupGenericListeners( editor, editor.addCommand( 'indent', new genericDefinition( true ) ) );
setupGenericListeners( editor, editor.addCommand( 'outdent', new genericDefinition() ) );
// Create and register toolbar button if possible.
if ( editor.ui.addButton ) {
editor.ui.addButton( 'Indent', {
label: editor.lang.indent.indent,
command: 'indent',
directional: true,
toolbar: 'indent,20'
} );
editor.ui.addButton( 'Outdent', {
label: editor.lang.indent.outdent,
command: 'outdent',
directional: true,
toolbar: 'indent,10'
} );
}
// Register dirChanged listener.
editor.on( 'dirChanged', function( evt ) {
var range = editor.createRange(),
dataNode = evt.data.node;
range.setStartBefore( dataNode );
range.setEndAfter( dataNode );
var walker = new CKEDITOR.dom.walker( range ),
node;
while ( ( node = walker.next() ) ) {
if ( node.type == CKEDITOR.NODE_ELEMENT ) {
// A child with the defined dir is to be ignored.
if ( !node.equals( dataNode ) && node.getDirection() ) {
range.setStartAfter( node );
walker = new CKEDITOR.dom.walker( range );
continue;
}
// Switch alignment classes.
var classes = editor.config.indentClasses;
if ( classes ) {
var suffix = ( evt.data.dir == 'ltr' ) ? [ '_rtl', '' ] : [ '', '_rtl' ];
for ( var i = 0; i < classes.length; i++ ) {
if ( node.hasClass( classes[ i ] + suffix[ 0 ] ) ) {
node.removeClass( classes[ i ] + suffix[ 0 ] );
node.addClass( classes[ i ] + suffix[ 1 ] );
}
}
}
// Switch the margins.
var marginLeft = node.getStyle( 'margin-right' ),
marginRight = node.getStyle( 'margin-left' );
marginLeft ? node.setStyle( 'margin-left', marginLeft ) : node.removeStyle( 'margin-left' );
marginRight ? node.setStyle( 'margin-right', marginRight ) : node.removeStyle( 'margin-right' );
}
}
} );
}
} );
/**
* Global command class definitions and global helpers.
*
* @class
* @singleton
*/
CKEDITOR.plugins.indent = {
/**
* A base class for a generic command definition, responsible mainly for creating
* Increase Indent and Decrease Indent toolbar buttons as well as for refreshing
* UI states.
*
* Commands of this class do not perform any indentation by themselves. They
* delegate this job to content-specific indentation commands (i.e. indentlist).
*
* @class CKEDITOR.plugins.indent.genericDefinition
* @extends CKEDITOR.commandDefinition
* @param {CKEDITOR.editor} editor The editor instance this command will be
* applied to.
* @param {String} name The name of the command.
* @param {Boolean} [isIndent] Defines the command as indenting or outdenting.
*/
genericDefinition: function( isIndent ) {
/**
* Determines whether the command belongs to the indentation family.
* Otherwise it is assumed to be an outdenting command.
*
* @readonly
* @property {Boolean} [=false]
*/
this.isIndent = !!isIndent;
// Mimic naive startDisabled behavior for outdent.
this.startDisabled = !this.isIndent;
},
/**
* A base class for specific indentation command definitions responsible for
* handling a pre-defined set of elements i.e. indentlist for lists or
* indentblock for text block elements.
*
* Commands of this class perform indentation operations and modify the DOM structure.
* They listen for events fired by {@link CKEDITOR.plugins.indent.genericDefinition}
* and execute defined actions.
*
* **NOTE**: This is not an {@link CKEDITOR.command editor command}.
* Context-specific commands are internal, for indentation system only.
*
* @class CKEDITOR.plugins.indent.specificDefinition
* @param {CKEDITOR.editor} editor The editor instance this command will be
* applied to.
* @param {String} name The name of the command.
* @param {Boolean} [isIndent] Defines the command as indenting or outdenting.
*/
specificDefinition: function( editor, name, isIndent ) {
this.name = name;
this.editor = editor;
/**
* An object of jobs handled by the command. Each job consists
* of two functions: `refresh` and `exec` as well as the execution priority.
*
* * The `refresh` function determines whether a job is doable for
* a particular context. These functions are executed in the
* order of priorities, one by one, for all plugins that registered
* jobs. As jobs are related to generic commands, refreshing
* occurs when the global command is firing the `refresh` event.
*
* **Note**: This function must return either {@link CKEDITOR#TRISTATE_DISABLED}
* or {@link CKEDITOR#TRISTATE_OFF}.
*
* * The `exec` function modifies the DOM if possible. Just like
* `refresh`, `exec` functions are executed in the order of priorities
* while the generic command is executed. This function is not executed
* if `refresh` for this job returned {@link CKEDITOR#TRISTATE_DISABLED}.
*
* **Note**: This function must return a Boolean value, indicating whether it
* was successful. If a job was successful, then no other jobs are being executed.
*
* Sample definition:
*
* command.jobs = {
* // Priority = 20.
* '20': {
* refresh( editor, path ) {
* if ( condition )
* return CKEDITOR.TRISTATE_OFF;
* else
* return CKEDITOR.TRISTATE_DISABLED;
* },
* exec( editor ) {
* // DOM modified! This was OK.
* return true;
* }
* },
* // Priority = 60. This job is done later.
* '60': {
* // Another job.
* }
* };
*
* For additional information, please check comments for
* the `setupGenericListeners` function.
*
* @readonly
* @property {Object} [={}]
*/
this.jobs = {};
/**
* Determines whether the editor that the command belongs to has
* {@link CKEDITOR.config#enterMode config.enterMode} set to {@link CKEDITOR#ENTER_BR}.
*
* @readonly
* @see CKEDITOR.config#enterMode
* @property {Boolean} [=false]
*/
this.enterBr = editor.config.enterMode == CKEDITOR.ENTER_BR;
/**
* Determines whether the command belongs to the indentation family.
* Otherwise it is assumed to be an outdenting command.
*
* @readonly
* @property {Boolean} [=false]
*/
this.isIndent = !!isIndent;
/**
* The name of the global command related to this one.
*
* @readonly
*/
this.relatedGlobal = isIndent ? 'indent' : 'outdent';
/**
* A keystroke associated with this command (*Tab* or *Shift+Tab*).
*
* @readonly
*/
this.indentKey = isIndent ? 9 : CKEDITOR.SHIFT + 9;
/**
* Stores created markers for the command so they can eventually be
* purged after the `exec` function is run.
*/
this.database = {};
},
/**
* Registers content-specific commands as a part of the indentation system
* directed by generic commands. Once a command is registered,
* it listens for events of a related generic command.
*
* CKEDITOR.plugins.indent.registerCommands( editor, {
* 'indentlist': new indentListCommand( editor, 'indentlist' ),
* 'outdentlist': new indentListCommand( editor, 'outdentlist' )
* } );
*
* Content-specific commands listen for the generic command's `exec` and
* try to execute their own jobs, one after another. If some execution is
* successful, `evt.data.done` is set so no more jobs (commands) are involved.
*
* Content-specific commands also listen for the generic command's `refresh`
* and fill the `evt.data.states` object with states of jobs. A generic command
* uses this data to determine its own state and to update the UI.
*
* @member CKEDITOR.plugins.indent
* @param {CKEDITOR.editor} editor The editor instance this command is
* applied to.
* @param {Object} commands An object of {@link CKEDITOR.command}.
*/
registerCommands: function( editor, commands ) {
editor.on( 'pluginsLoaded', function() {
for ( var name in commands ) {
( function( editor, command ) {
var relatedGlobal = editor.getCommand( command.relatedGlobal );
for ( var priority in command.jobs ) {
// Observe generic exec event and execute command when necessary.
// If the command was successfully handled by the command and
// DOM has been modified, stop event propagation so no other plugin
// will bother. Job is done.
relatedGlobal.on( 'exec', function( evt ) {
if ( evt.data.done )
return;
// Make sure that anything this command will do is invisible
// for undoManager. What undoManager only can see and
// remember is the execution of the global command (relatedGlobal).
editor.fire( 'lockSnapshot' );
if ( command.execJob( editor, priority ) )
evt.data.done = true;
editor.fire( 'unlockSnapshot' );
// Clean up the markers.
CKEDITOR.dom.element.clearAllMarkers( command.database );
}, this, null, priority );
// Observe generic refresh event and force command refresh.
// Once refreshed, save command state in event data
// so generic command plugin can update its own state and UI.
relatedGlobal.on( 'refresh', function( evt ) {
if ( !evt.data.states )
evt.data.states = {};
evt.data.states[ command.name + '@' + priority ] =
command.refreshJob( editor, priority, evt.data.path );
}, this, null, priority );
}
// Since specific indent commands have no UI elements,
// they need to be manually registered as a editor feature.
editor.addFeature( command );
} )( this, commands[ name ] );
}
} );
}
};
CKEDITOR.plugins.indent.genericDefinition.prototype = {
context: 'p',
exec: function() {}
};
CKEDITOR.plugins.indent.specificDefinition.prototype = {
/**
* Executes the content-specific procedure if the context is correct.
* It calls the `exec` function of a job of the given `priority`
* that modifies the DOM.
*
* @param {CKEDITOR.editor} editor The editor instance this command
* will be applied to.
* @param {Number} priority The priority of the job to be executed.
* @returns {Boolean} Indicates whether the job was successful.
*/
execJob: function( editor, priority ) {
var job = this.jobs[ priority ];
if ( job.state != TRISTATE_DISABLED )
return job.exec.call( this, editor );
},
/**
* Calls the `refresh` function of a job of the given `priority`.
* The function returns the state of the job which can be either
* {@link CKEDITOR#TRISTATE_DISABLED} or {@link CKEDITOR#TRISTATE_OFF}.
*
* @param {CKEDITOR.editor} editor The editor instance this command
* will be applied to.
* @param {Number} priority The priority of the job to be executed.
* @returns {Number} The state of the job.
*/
refreshJob: function( editor, priority, path ) {
var job = this.jobs[ priority ];
if ( !editor.activeFilter.checkFeature( this ) )
job.state = TRISTATE_DISABLED;
else
job.state = job.refresh.call( this, editor, path );
return job.state;
},
/**
* Checks if the element path contains the element handled
* by this indentation command.
*
* @param {CKEDITOR.dom.elementPath} node A path to be checked.
* @returns {CKEDITOR.dom.element}
*/
getContext: function( path ) {
return path.contains( this.context );
}
};
/**
* Attaches event listeners for this generic command. Since the indentation
* system is event-oriented, generic commands communicate with
* content-specific commands using the `exec` and `refresh` events.
*
* Listener priorities are crucial. Different indentation phases
* are executed with different priorities.
*
* For the `exec` event:
*
* * 0: Selection and bookmarks are saved by the generic command.
* * 1-99: Content-specific commands try to indent the code by executing
* their own jobs ({@link CKEDITOR.plugins.indent.specificDefinition#jobs}).
* * 100: Bookmarks are re-selected by the generic command.
*
* The visual interpretation looks as follows:
*
* +------------------+
* | Exec event fired |
* +------ + ---------+
* |
* 0 -<----------+ Selection and bookmarks saved.
* |
* |
* 25 -<---+ Exec 1st job of plugin#1 (return false, continuing...).
* |
* |
* 50 -<---+ Exec 1st job of plugin#2 (return false, continuing...).
* |
* |
* 75 -<---+ Exec 2nd job of plugin#1 (only if plugin#2 failed).
* |
* |
* 100 -<-----------+ Re-select bookmarks, clean-up.
* |
* +-------- v ----------+
* | Exec event finished |
* +---------------------+
*
* For the `refresh` event:
*
* * <100: Content-specific commands refresh their job states according
* to the given path. Jobs save their states in the `evt.data.states` object
* passed along with the event. This can be either {@link CKEDITOR#TRISTATE_DISABLED}
* or {@link CKEDITOR#TRISTATE_OFF}.
* * 100: Command state is determined according to what states
* have been returned by content-specific jobs (`evt.data.states`).
* UI elements are updated at this stage.
*
* **Note**: If there is at least one job with the {@link CKEDITOR#TRISTATE_OFF} state,
* then the generic command state is also {@link CKEDITOR#TRISTATE_OFF}. Otherwise,
* the command state is {@link CKEDITOR#TRISTATE_DISABLED}.
*
* @param {CKEDITOR.command} command The command to be set up.
* @private
*/
function setupGenericListeners( editor, command ) {
var selection, bookmarks;
// Set the command state according to content-specific
// command states.
command.on( 'refresh', function( evt ) {
// If no state comes with event data, disable command.
var states = [ TRISTATE_DISABLED ];
for ( var s in evt.data.states )
states.push( evt.data.states[ s ] );
this.setState( CKEDITOR.tools.search( states, TRISTATE_OFF ) ?
TRISTATE_OFF
:
TRISTATE_DISABLED );
}, command, null, 100 );
// Initialization. Save bookmarks and mark event as not handled
// by any plugin (command) yet.
command.on( 'exec', function( evt ) {
selection = editor.getSelection();
bookmarks = selection.createBookmarks( 1 );
// Mark execution as not handled yet.
if ( !evt.data )
evt.data = {};
evt.data.done = false;
}, command, null, 0 );
// Housekeeping. Make sure selectionChange will be called.
// Also re-select previously saved bookmarks.
command.on( 'exec', function( evt ) {
editor.forceNextSelectionCheck();
selection.selectBookmarks( bookmarks );
}, command, null, 100 );
}
} )();

View File

@ -0,0 +1,298 @@
/**
* @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
/**
* @fileOverview Handles the indentation of lists.
*/
( function() {
'use strict';
var isNotWhitespaces = CKEDITOR.dom.walker.whitespaces( true ),
isNotBookmark = CKEDITOR.dom.walker.bookmark( false, true ),
TRISTATE_DISABLED = CKEDITOR.TRISTATE_DISABLED,
TRISTATE_OFF = CKEDITOR.TRISTATE_OFF;
CKEDITOR.plugins.add( 'indentlist', {
requires: 'indent',
init: function( editor ) {
var globalHelpers = CKEDITOR.plugins.indent,
editable = editor;
// Register commands.
globalHelpers.registerCommands( editor, {
indentlist: new commandDefinition( editor, 'indentlist', true ),
outdentlist: new commandDefinition( editor, 'outdentlist' )
} );
function commandDefinition( editor, name ) {
globalHelpers.specificDefinition.apply( this, arguments );
// Require ul OR ol list.
this.requiredContent = [ 'ul', 'ol' ];
// Indent and outdent lists with TAB/SHIFT+TAB key. Indenting can
// be done for any list item that isn't the first child of the parent.
editor.on( 'key', function( evt ) {
if ( editor.mode != 'wysiwyg' )
return;
if ( evt.data.keyCode == this.indentKey ) {
var list = this.getContext( editor.elementPath() );
if ( list ) {
// Don't indent if in first list item of the parent.
// Outdent, however, can always be done to collapse
// the list into a paragraph (div).
if ( this.isIndent && firstItemInPath.call( this, editor.elementPath(), list ) )
return;
// Exec related global indentation command. Global
// commands take care of bookmarks and selection,
// so it's much easier to use them instead of
// content-specific commands.
editor.execCommand( this.relatedGlobal );
// Cancel the key event so editor doesn't lose focus.
evt.cancel();
}
}
}, this );
// There are two different jobs for this plugin:
//
// * Indent job (priority=10), before indentblock.
//
// This job is before indentblock because, if this plugin is
// loaded it has higher priority over indentblock. It means that,
// if possible, nesting is performed, and then block manipulation,
// if necessary.
//
// * Outdent job (priority=30), after outdentblock.
//
// This job got to be after outdentblock because in some cases
// (margin, config#indentClass on list) outdent must be done on
// block-level.
this.jobs[ this.isIndent ? 10 : 30 ] = {
refresh: this.isIndent ?
function( editor, path ) {
var list = this.getContext( path ),
inFirstListItem = firstItemInPath.call( this, path, list );
if ( !list || !this.isIndent || inFirstListItem )
return TRISTATE_DISABLED;
return TRISTATE_OFF;
}
:
function( editor, path ) {
var list = this.getContext( path );
if ( !list || this.isIndent )
return TRISTATE_DISABLED;
return TRISTATE_OFF;
},
exec: CKEDITOR.tools.bind( indentList, this )
};
}
CKEDITOR.tools.extend( commandDefinition.prototype, globalHelpers.specificDefinition.prototype, {
// Elements that, if in an elementpath, will be handled by this
// command. They restrict the scope of the plugin.
context: { ol: 1, ul: 1 }
} );
}
} );
function indentList( editor ) {
var that = this,
database = this.database,
context = this.context;
function indent( listNode ) {
// Our starting and ending points of the range might be inside some blocks under a list item...
// So before playing with the iterator, we need to expand the block to include the list items.
var startContainer = range.startContainer,
endContainer = range.endContainer;
while ( startContainer && !startContainer.getParent().equals( listNode ) )
startContainer = startContainer.getParent();
while ( endContainer && !endContainer.getParent().equals( listNode ) )
endContainer = endContainer.getParent();
if ( !startContainer || !endContainer )
return false;
// Now we can iterate over the individual items on the same tree depth.
var block = startContainer,
itemsToMove = [],
stopFlag = false;
while ( !stopFlag ) {
if ( block.equals( endContainer ) )
stopFlag = true;
itemsToMove.push( block );
block = block.getNext();
}
if ( itemsToMove.length < 1 )
return false;
// Do indent or outdent operations on the array model of the list, not the
// list's DOM tree itself. The array model demands that it knows as much as
// possible about the surrounding lists, we need to feed it the further
// ancestor node that is still a list.
var listParents = listNode.getParents( true );
for ( var i = 0; i < listParents.length; i++ ) {
if ( listParents[ i ].getName && context[ listParents[ i ].getName() ] ) {
listNode = listParents[ i ];
break;
}
}
var indentOffset = that.isIndent ? 1 : -1,
startItem = itemsToMove[ 0 ],
lastItem = itemsToMove[ itemsToMove.length - 1 ],
// Convert the list DOM tree into a one dimensional array.
listArray = CKEDITOR.plugins.list.listToArray( listNode, database ),
// Apply indenting or outdenting on the array.
baseIndent = listArray[ lastItem.getCustomData( 'listarray_index' ) ].indent;
for ( i = startItem.getCustomData( 'listarray_index' ); i <= lastItem.getCustomData( 'listarray_index' ); i++ ) {
listArray[ i ].indent += indentOffset;
// Make sure the newly created sublist get a brand-new element of the same type. (#5372)
if ( indentOffset > 0 ) {
var listRoot = listArray[ i ].parent;
listArray[ i ].parent = new CKEDITOR.dom.element( listRoot.getName(), listRoot.getDocument() );
}
}
for ( i = lastItem.getCustomData( 'listarray_index' ) + 1; i < listArray.length && listArray[ i ].indent > baseIndent; i++ )
listArray[ i ].indent += indentOffset;
// Convert the array back to a DOM forest (yes we might have a few subtrees now).
// And replace the old list with the new forest.
var newList = CKEDITOR.plugins.list.arrayToList( listArray, database, null, editor.config.enterMode, listNode.getDirection() );
// Avoid nested <li> after outdent even they're visually same,
// recording them for later refactoring.(#3982)
if ( !that.isIndent ) {
var parentLiElement;
if ( ( parentLiElement = listNode.getParent() ) && parentLiElement.is( 'li' ) ) {
var children = newList.listNode.getChildren(),
pendingLis = [],
count = children.count(),
child;
for ( i = count - 1; i >= 0; i-- ) {
if ( ( child = children.getItem( i ) ) && child.is && child.is( 'li' ) )
pendingLis.push( child );
}
}
}
if ( newList )
newList.listNode.replace( listNode );
// Move the nested <li> to be appeared after the parent.
if ( pendingLis && pendingLis.length ) {
for ( i = 0; i < pendingLis.length; i++ ) {
var li = pendingLis[ i ],
followingList = li;
// Nest preceding <ul>/<ol> inside current <li> if any.
while ( ( followingList = followingList.getNext() ) && followingList.is && followingList.getName() in context ) {
// IE requires a filler NBSP for nested list inside empty list item,
// otherwise the list item will be inaccessiable. (#4476)
if ( CKEDITOR.env.needsNbspFiller && !li.getFirst( neitherWhitespacesNorBookmark ) )
li.append( range.document.createText( '\u00a0' ) );
li.append( followingList );
}
li.insertAfter( parentLiElement );
}
}
if ( newList )
editor.fire( 'contentDomInvalidated' );
return true;
}
var selection = editor.getSelection(),
ranges = selection && selection.getRanges(),
iterator = ranges.createIterator(),
range;
while ( ( range = iterator.getNextRange() ) ) {
var rangeRoot = range.getCommonAncestor(),
nearestListBlock = rangeRoot;
while ( nearestListBlock && !( nearestListBlock.type == CKEDITOR.NODE_ELEMENT && context[ nearestListBlock.getName() ] ) )
nearestListBlock = nearestListBlock.getParent();
// Avoid having selection boundaries out of the list.
// <ul><li>[...</li></ul><p>...]</p> => <ul><li>[...]</li></ul><p>...</p>
if ( !nearestListBlock ) {
if ( ( nearestListBlock = range.startPath().contains( context ) ) )
range.setEndAt( nearestListBlock, CKEDITOR.POSITION_BEFORE_END );
}
// Avoid having selection enclose the entire list. (#6138)
// [<ul><li>...</li></ul>] =><ul><li>[...]</li></ul>
if ( !nearestListBlock ) {
var selectedNode = range.getEnclosedNode();
if ( selectedNode && selectedNode.type == CKEDITOR.NODE_ELEMENT && selectedNode.getName() in context ) {
range.setStartAt( selectedNode, CKEDITOR.POSITION_AFTER_START );
range.setEndAt( selectedNode, CKEDITOR.POSITION_BEFORE_END );
nearestListBlock = selectedNode;
}
}
// Avoid selection anchors under list root.
// <ul>[<li>...</li>]</ul> => <ul><li>[...]</li></ul>
if ( nearestListBlock && range.startContainer.type == CKEDITOR.NODE_ELEMENT && range.startContainer.getName() in context ) {
var walker = new CKEDITOR.dom.walker( range );
walker.evaluator = listItem;
range.startContainer = walker.next();
}
if ( nearestListBlock && range.endContainer.type == CKEDITOR.NODE_ELEMENT && range.endContainer.getName() in context ) {
walker = new CKEDITOR.dom.walker( range );
walker.evaluator = listItem;
range.endContainer = walker.previous();
}
if ( nearestListBlock )
return indent( nearestListBlock );
}
return 0;
}
// Check whether a first child of a list is in the path.
// The list can be extracted from path or given explicitly
// e.g. for better performance if cached.
function firstItemInPath( path, list ) {
if ( !list )
list = path.contains( this.context );
return list && path.block && path.block.equals( list.getFirst( listItem ) );
}
// Determines whether a node is a list <li> element.
function listItem( node ) {
return node.type == CKEDITOR.NODE_ELEMENT && node.is( 'li' );
}
function neitherWhitespacesNorBookmark( node ) {
return isNotWhitespaces( node ) && isNotBookmark( node );
}
} )();

View File

@ -0,0 +1,241 @@
/**
* @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
/**
* @fileOverview Justify commands.
*/
( function() {
function getAlignment( element, useComputedState ) {
useComputedState = useComputedState === undefined || useComputedState;
var align;
if ( useComputedState )
align = element.getComputedStyle( 'text-align' );
else {
while ( !element.hasAttribute || !( element.hasAttribute( 'align' ) || element.getStyle( 'text-align' ) ) ) {
var parent = element.getParent();
if ( !parent )
break;
element = parent;
}
align = element.getStyle( 'text-align' ) || element.getAttribute( 'align' ) || '';
}
// Sometimes computed values doesn't tell.
align && ( align = align.replace( /(?:-(?:moz|webkit)-)?(?:start|auto)/i, '' ) );
!align && useComputedState && ( align = element.getComputedStyle( 'direction' ) == 'rtl' ? 'right' : 'left' );
return align;
}
function justifyCommand( editor, name, value ) {
this.editor = editor;
this.name = name;
this.value = value;
this.context = 'p';
var classes = editor.config.justifyClasses,
blockTag = editor.config.enterMode == CKEDITOR.ENTER_P ? 'p' : 'div';
if ( classes ) {
switch ( value ) {
case 'left':
this.cssClassName = classes[ 0 ];
break;
case 'center':
this.cssClassName = classes[ 1 ];
break;
case 'right':
this.cssClassName = classes[ 2 ];
break;
case 'justify':
this.cssClassName = classes[ 3 ];
break;
}
this.cssClassRegex = new RegExp( '(?:^|\\s+)(?:' + classes.join( '|' ) + ')(?=$|\\s)' );
this.requiredContent = blockTag + '(' + this.cssClassName + ')';
}
else
this.requiredContent = blockTag + '{text-align}';
this.allowedContent = {
'caption div h1 h2 h3 h4 h5 h6 p pre td th li': {
// Do not add elements, but only text-align style if element is validated by other rule.
propertiesOnly: true,
styles: this.cssClassName ? null : 'text-align',
classes: this.cssClassName || null
}
};
// In enter mode BR we need to allow here for div, because when non other
// feature allows div justify is the only plugin that uses it.
if ( editor.config.enterMode == CKEDITOR.ENTER_BR )
this.allowedContent.div = true;
}
function onDirChanged( e ) {
var editor = e.editor;
var range = editor.createRange();
range.setStartBefore( e.data.node );
range.setEndAfter( e.data.node );
var walker = new CKEDITOR.dom.walker( range ),
node;
while ( ( node = walker.next() ) ) {
if ( node.type == CKEDITOR.NODE_ELEMENT ) {
// A child with the defined dir is to be ignored.
if ( !node.equals( e.data.node ) && node.getDirection() ) {
range.setStartAfter( node );
walker = new CKEDITOR.dom.walker( range );
continue;
}
// Switch the alignment.
var classes = editor.config.justifyClasses;
if ( classes ) {
// The left align class.
if ( node.hasClass( classes[ 0 ] ) ) {
node.removeClass( classes[ 0 ] );
node.addClass( classes[ 2 ] );
}
// The right align class.
else if ( node.hasClass( classes[ 2 ] ) ) {
node.removeClass( classes[ 2 ] );
node.addClass( classes[ 0 ] );
}
}
// Always switch CSS margins.
var style = 'text-align';
var align = node.getStyle( style );
if ( align == 'left' )
node.setStyle( style, 'right' );
else if ( align == 'right' )
node.setStyle( style, 'left' );
}
}
}
justifyCommand.prototype = {
exec: function( editor ) {
var selection = editor.getSelection(),
enterMode = editor.config.enterMode;
if ( !selection )
return;
var bookmarks = selection.createBookmarks(),
ranges = selection.getRanges();
var cssClassName = this.cssClassName,
iterator, block;
var useComputedState = editor.config.useComputedState;
useComputedState = useComputedState === undefined || useComputedState;
for ( var i = ranges.length - 1; i >= 0; i-- ) {
iterator = ranges[ i ].createIterator();
iterator.enlargeBr = enterMode != CKEDITOR.ENTER_BR;
while ( ( block = iterator.getNextParagraph( enterMode == CKEDITOR.ENTER_P ? 'p' : 'div' ) ) ) {
if ( block.isReadOnly() )
continue;
block.removeAttribute( 'align' );
block.removeStyle( 'text-align' );
// Remove any of the alignment classes from the className.
var className = cssClassName && ( block.$.className = CKEDITOR.tools.ltrim( block.$.className.replace( this.cssClassRegex, '' ) ) );
var apply = ( this.state == CKEDITOR.TRISTATE_OFF ) && ( !useComputedState || ( getAlignment( block, true ) != this.value ) );
if ( cssClassName ) {
// Append the desired class name.
if ( apply )
block.addClass( cssClassName );
else if ( !className )
block.removeAttribute( 'class' );
} else if ( apply )
block.setStyle( 'text-align', this.value );
}
}
editor.focus();
editor.forceNextSelectionCheck();
selection.selectBookmarks( bookmarks );
},
refresh: function( editor, path ) {
var firstBlock = path.block || path.blockLimit;
this.setState( firstBlock.getName() != 'body' && getAlignment( firstBlock, this.editor.config.useComputedState ) == this.value ? CKEDITOR.TRISTATE_ON : CKEDITOR.TRISTATE_OFF );
}
};
CKEDITOR.plugins.add( 'justify', {
lang: 'af,ar,bg,bn,bs,ca,cs,cy,da,de,el,en,en-au,en-ca,en-gb,eo,es,et,eu,fa,fi,fo,fr,fr-ca,gl,gu,he,hi,hr,hu,id,is,it,ja,ka,km,ko,ku,lt,lv,mk,mn,ms,nb,nl,no,pl,pt,pt-br,ro,ru,si,sk,sl,sq,sr,sr-latn,sv,th,tr,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE%
icons: 'justifyblock,justifycenter,justifyleft,justifyright', // %REMOVE_LINE_CORE%
hidpi: true, // %REMOVE_LINE_CORE%
init: function( editor ) {
if ( editor.blockless )
return;
var left = new justifyCommand( editor, 'justifyleft', 'left' ),
center = new justifyCommand( editor, 'justifycenter', 'center' ),
right = new justifyCommand( editor, 'justifyright', 'right' ),
justify = new justifyCommand( editor, 'justifyblock', 'justify' );
editor.addCommand( 'justifyleft', left );
editor.addCommand( 'justifycenter', center );
editor.addCommand( 'justifyright', right );
editor.addCommand( 'justifyblock', justify );
if ( editor.ui.addButton ) {
editor.ui.addButton( 'JustifyLeft', {
label: editor.lang.justify.left,
command: 'justifyleft',
toolbar: 'align,10'
} );
editor.ui.addButton( 'JustifyCenter', {
label: editor.lang.justify.center,
command: 'justifycenter',
toolbar: 'align,20'
} );
editor.ui.addButton( 'JustifyRight', {
label: editor.lang.justify.right,
command: 'justifyright',
toolbar: 'align,30'
} );
editor.ui.addButton( 'JustifyBlock', {
label: editor.lang.justify.block,
command: 'justifyblock',
toolbar: 'align,40'
} );
}
editor.on( 'dirChanged', onDirChanged );
}
} );
} )();
/**
* List of classes to use for aligning the contents. If it's `null`, no classes will be used
* and instead the corresponding CSS values will be used.
*
* The array should contain 4 members, in the following order: left, center, right, justify.
*
* // Use the classes 'AlignLeft', 'AlignCenter', 'AlignRight', 'AlignJustify'
* config.justifyClasses = [ 'AlignLeft', 'AlignCenter', 'AlignRight', 'AlignJustify' ];
*
* @cfg {Array} [justifyClasses=null]
* @member CKEDITOR.config
*/

View File

@ -0,0 +1,422 @@
/**
* @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
CKEDITOR.plugins.add( 'link', {
requires: 'dialog,fakeobjects',
lang: 'af,ar,bg,bn,bs,ca,cs,cy,da,de,el,en,en-au,en-ca,en-gb,eo,es,et,eu,fa,fi,fo,fr,fr-ca,gl,gu,he,hi,hr,hu,id,is,it,ja,ka,km,ko,ku,lt,lv,mk,mn,ms,nb,nl,no,pl,pt,pt-br,ro,ru,si,sk,sl,sq,sr,sr-latn,sv,th,tr,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE%
icons: 'anchor,anchor-rtl,link,unlink', // %REMOVE_LINE_CORE%
hidpi: true, // %REMOVE_LINE_CORE%
onLoad: function() {
// Add the CSS styles for anchor placeholders.
var iconPath = CKEDITOR.getUrl( this.path + 'images' + ( CKEDITOR.env.hidpi ? '/hidpi' : '' ) + '/anchor.png' ),
baseStyle = 'background:url(' + iconPath + ') no-repeat %1 center;border:1px dotted #00f;background-size:16px;';
var template = '.%2 a.cke_anchor,' +
'.%2 a.cke_anchor_empty' +
',.cke_editable.%2 a[name]' +
',.cke_editable.%2 a[data-cke-saved-name]' +
'{' +
baseStyle +
'padding-%1:18px;' +
// Show the arrow cursor for the anchor image (FF at least).
'cursor:auto;' +
'}' +
( CKEDITOR.plugins.link.synAnchorSelector ? ( 'a.cke_anchor_empty' +
'{' +
// Make empty anchor selectable on IE.
'display:inline-block;' +
// IE11 doesn't display empty inline-block elements.
( CKEDITOR.env.ie && CKEDITOR.env.version > 10 ? 'min-height:16px;vertical-align:middle' : '' ) +
'}'
) : '' ) +
'.%2 img.cke_anchor' +
'{' +
baseStyle +
'width:16px;' +
'min-height:15px;' +
// The default line-height on IE.
'height:1.15em;' +
// Opera works better with "middle" (even if not perfect)
'vertical-align:' + ( CKEDITOR.env.opera ? 'middle' : 'text-bottom' ) + ';' +
'}';
// Styles with contents direction awareness.
function cssWithDir( dir ) {
return template.replace( /%1/g, dir == 'rtl' ? 'right' : 'left' ).replace( /%2/g, 'cke_contents_' + dir );
}
CKEDITOR.addCss( cssWithDir( 'ltr' ) + cssWithDir( 'rtl' ) );
},
init: function( editor ) {
var allowed = 'a[!href]',
required = 'a[href]';
if ( CKEDITOR.dialog.isTabEnabled( editor, 'link', 'advanced' ) )
allowed = allowed.replace( ']', ',accesskey,charset,dir,id,lang,name,rel,tabindex,title,type]{*}(*)' );
if ( CKEDITOR.dialog.isTabEnabled( editor, 'link', 'target' ) )
allowed = allowed.replace( ']', ',target,onclick]' );
// Add the link and unlink buttons.
editor.addCommand( 'link', new CKEDITOR.dialogCommand( 'link', {
allowedContent: allowed,
requiredContent: required
} ) );
editor.addCommand( 'anchor', new CKEDITOR.dialogCommand( 'anchor', {
allowedContent: 'a[!name,id]',
requiredContent: 'a[name]'
} ) );
editor.addCommand( 'unlink', new CKEDITOR.unlinkCommand() );
editor.addCommand( 'removeAnchor', new CKEDITOR.removeAnchorCommand() );
editor.setKeystroke( CKEDITOR.CTRL + 76 /*L*/, 'link' );
if ( editor.ui.addButton ) {
editor.ui.addButton( 'Link', {
label: editor.lang.link.toolbar,
command: 'link',
toolbar: 'links,10'
} );
editor.ui.addButton( 'Unlink', {
label: editor.lang.link.unlink,
command: 'unlink',
toolbar: 'links,20'
} );
editor.ui.addButton( 'Anchor', {
label: editor.lang.link.anchor.toolbar,
command: 'anchor',
toolbar: 'links,30'
} );
}
CKEDITOR.dialog.add( 'link', this.path + 'dialogs/link.js' );
CKEDITOR.dialog.add( 'anchor', this.path + 'dialogs/anchor.js' );
editor.on( 'doubleclick', function( evt ) {
var element = CKEDITOR.plugins.link.getSelectedLink( editor ) || evt.data.element;
if ( !element.isReadOnly() ) {
if ( element.is( 'a' ) ) {
evt.data.dialog = ( element.getAttribute( 'name' ) && ( !element.getAttribute( 'href' ) || !element.getChildCount() ) ) ? 'anchor' : 'link';
editor.getSelection().selectElement( element );
} else if ( CKEDITOR.plugins.link.tryRestoreFakeAnchor( editor, element ) )
evt.data.dialog = 'anchor';
}
} );
// If the "menu" plugin is loaded, register the menu items.
if ( editor.addMenuItems ) {
editor.addMenuItems( {
anchor: {
label: editor.lang.link.anchor.menu,
command: 'anchor',
group: 'anchor',
order: 1
},
removeAnchor: {
label: editor.lang.link.anchor.remove,
command: 'removeAnchor',
group: 'anchor',
order: 5
},
link: {
label: editor.lang.link.menu,
command: 'link',
group: 'link',
order: 1
},
unlink: {
label: editor.lang.link.unlink,
command: 'unlink',
group: 'link',
order: 5
}
} );
}
// If the "contextmenu" plugin is loaded, register the listeners.
if ( editor.contextMenu ) {
editor.contextMenu.addListener( function( element, selection ) {
if ( !element || element.isReadOnly() )
return null;
var anchor = CKEDITOR.plugins.link.tryRestoreFakeAnchor( editor, element );
if ( !anchor && !( anchor = CKEDITOR.plugins.link.getSelectedLink( editor ) ) )
return null;
var menu = {};
if ( anchor.getAttribute( 'href' ) && anchor.getChildCount() )
menu = { link: CKEDITOR.TRISTATE_OFF, unlink: CKEDITOR.TRISTATE_OFF };
if ( anchor && anchor.hasAttribute( 'name' ) )
menu.anchor = menu.removeAnchor = CKEDITOR.TRISTATE_OFF;
return menu;
} );
}
},
afterInit: function( editor ) {
// Register a filter to displaying placeholders after mode change.
var dataProcessor = editor.dataProcessor,
dataFilter = dataProcessor && dataProcessor.dataFilter,
htmlFilter = dataProcessor && dataProcessor.htmlFilter,
pathFilters = editor._.elementsPath && editor._.elementsPath.filters;
if ( dataFilter ) {
dataFilter.addRules( {
elements: {
a: function( element ) {
var attributes = element.attributes;
if ( !attributes.name )
return null;
var isEmpty = !element.children.length;
if ( CKEDITOR.plugins.link.synAnchorSelector ) {
// IE needs a specific class name to be applied
// to the anchors, for appropriate styling.
var ieClass = isEmpty ? 'cke_anchor_empty' : 'cke_anchor';
var cls = attributes[ 'class' ];
if ( attributes.name && ( !cls || cls.indexOf( ieClass ) < 0 ) )
attributes[ 'class' ] = ( cls || '' ) + ' ' + ieClass;
if ( isEmpty && CKEDITOR.plugins.link.emptyAnchorFix ) {
attributes.contenteditable = 'false';
attributes[ 'data-cke-editable' ] = 1;
}
} else if ( CKEDITOR.plugins.link.fakeAnchor && isEmpty )
return editor.createFakeParserElement( element, 'cke_anchor', 'anchor' );
return null;
}
}
} );
}
if ( CKEDITOR.plugins.link.emptyAnchorFix && htmlFilter ) {
htmlFilter.addRules( {
elements: {
a: function( element ) {
delete element.attributes.contenteditable;
}
}
} );
}
if ( pathFilters ) {
pathFilters.push( function( element, name ) {
if ( name == 'a' ) {
if ( CKEDITOR.plugins.link.tryRestoreFakeAnchor( editor, element ) || ( element.getAttribute( 'name' ) && ( !element.getAttribute( 'href' ) || !element.getChildCount() ) ) )
return 'anchor';
}
} );
}
}
} );
/**
* Set of Link plugin helpers.
*
* @class
* @singleton
*/
CKEDITOR.plugins.link = {
/**
* Get the surrounding link element of the current selection.
*
* CKEDITOR.plugins.link.getSelectedLink( editor );
*
* // The following selections will all return the link element.
*
* <a href="#">li^nk</a>
* <a href="#">[link]</a>
* text[<a href="#">link]</a>
* <a href="#">li[nk</a>]
* [<b><a href="#">li]nk</a></b>]
* [<a href="#"><b>li]nk</b></a>
*
* @since 3.2.1
* @param {CKEDITOR.editor} editor
*/
getSelectedLink: function( editor ) {
var selection = editor.getSelection();
var selectedElement = selection.getSelectedElement();
if ( selectedElement && selectedElement.is( 'a' ) )
return selectedElement;
var range = selection.getRanges()[ 0 ];
if ( range ) {
range.shrink( CKEDITOR.SHRINK_TEXT );
return editor.elementPath( range.getCommonAncestor() ).contains( 'a', 1 );
}
return null;
},
/**
* Collects anchors available in the editor (i.e. used by the Link plugin).
* Note that the scope of search is different for inline (the "global" document) and
* classic (`iframe`-based) editors (the "inner" document).
*
* @since 4.3.3
* @param {CKEDITOR.editor} editor
* @returns {CKEDITOR.dom.element[]} An array of anchor elements.
*/
getEditorAnchors: function( editor ) {
var editable = editor.editable(),
// The scope of search for anchors is the entire document for inline editors
// and editor's editable for classic editor/divarea (#11359).
scope = ( editable.isInline() && !editor.plugins.divarea ) ? editor.document : editable,
links = scope.getElementsByTag( 'a' ),
anchors = [],
i = 0,
item;
// Retrieve all anchors within the scope.
while ( ( item = links.getItem( i++ ) ) ) {
if ( item.data( 'cke-saved-name' ) || item.hasAttribute( 'name' ) ) {
anchors.push( {
name: item.data( 'cke-saved-name' ) || item.getAttribute( 'name' ),
id: item.getAttribute( 'id' )
} );
}
}
// Retrieve all "fake anchors" within the scope.
if ( this.fakeAnchor ) {
var imgs = scope.getElementsByTag( 'img' );
i = 0;
while ( ( item = imgs.getItem( i++ ) ) ) {
if ( ( item = this.tryRestoreFakeAnchor( editor, item ) ) ) {
anchors.push( {
name: item.getAttribute( 'name' ),
id: item.getAttribute( 'id' )
} );
}
}
}
return anchors;
},
/**
* Opera and WebKit do not make it possible to select empty anchors. Fake
* elements must be used for them.
*
* @readonly
* @property {Boolean}
*/
fakeAnchor: CKEDITOR.env.opera || CKEDITOR.env.webkit,
/**
* For browsers that do not support CSS3 `a[name]:empty()`. Note that IE9 is included because of #7783.
*
* @readonly
* @property {Boolean}
*/
synAnchorSelector: CKEDITOR.env.ie,
/**
* For browsers that have editing issues with an empty anchor.
*
* @readonly
* @property {Boolean}
*/
emptyAnchorFix: CKEDITOR.env.ie && CKEDITOR.env.version < 8,
/**
* Returns an element representing a real anchor restored from a fake anchor.
*
* @param {CKEDITOR.editor} editor
* @param {CKEDITOR.dom.element} element
* @returns {CKEDITOR.dom.element} Restored anchor element or nothing if the
* passed element was not a fake anchor.
*/
tryRestoreFakeAnchor: function( editor, element ) {
if ( element && element.data( 'cke-real-element-type' ) && element.data( 'cke-real-element-type' ) == 'anchor' ) {
var link = editor.restoreRealElement( element );
if ( link.data( 'cke-saved-name' ) )
return link;
}
}
};
// TODO Much probably there's no need to expose these as public objects.
CKEDITOR.unlinkCommand = function() {};
CKEDITOR.unlinkCommand.prototype = {
exec: function( editor ) {
var style = new CKEDITOR.style( { element: 'a', type: CKEDITOR.STYLE_INLINE, alwaysRemoveElement: 1 } );
editor.removeStyle( style );
},
refresh: function( editor, path ) {
// Despite our initial hope, document.queryCommandEnabled() does not work
// for this in Firefox. So we must detect the state by element paths.
var element = path.lastElement && path.lastElement.getAscendant( 'a', true );
if ( element && element.getName() == 'a' && element.getAttribute( 'href' ) && element.getChildCount() )
this.setState( CKEDITOR.TRISTATE_OFF );
else
this.setState( CKEDITOR.TRISTATE_DISABLED );
},
contextSensitive: 1,
startDisabled: 1,
requiredContent: 'a[href]'
};
CKEDITOR.removeAnchorCommand = function() {};
CKEDITOR.removeAnchorCommand.prototype = {
exec: function( editor ) {
var sel = editor.getSelection(),
bms = sel.createBookmarks(),
anchor;
if ( sel && ( anchor = sel.getSelectedElement() ) && ( CKEDITOR.plugins.link.fakeAnchor && !anchor.getChildCount() ? CKEDITOR.plugins.link.tryRestoreFakeAnchor( editor, anchor ) : anchor.is( 'a' ) ) )
anchor.remove( 1 );
else {
if ( ( anchor = CKEDITOR.plugins.link.getSelectedLink( editor ) ) ) {
if ( anchor.hasAttribute( 'href' ) ) {
anchor.removeAttributes( { name: 1, 'data-cke-saved-name': 1 } );
anchor.removeClass( 'cke_anchor' );
} else
anchor.remove( 1 );
}
}
sel.selectBookmarks( bms );
},
requiredContent: 'a[name]'
};
CKEDITOR.tools.extend( CKEDITOR.config, {
/**
* Whether to show the Advanced tab in the Link dialog window.
*
* @cfg {Boolean} [linkShowAdvancedTab=true]
* @member CKEDITOR.config
*/
linkShowAdvancedTab: true,
/**
* Whether to show the Target tab in the Link dialog window.
*
* @cfg {Boolean} [linkShowTargetTab=true]
* @member CKEDITOR.config
*/
linkShowTargetTab: true
} );

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,240 @@
/**
* @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
CKEDITOR.plugins.add( 'listblock', {
requires: 'panel',
onLoad: function() {
var list = CKEDITOR.addTemplate( 'panel-list', '<ul role="presentation" class="cke_panel_list">{items}</ul>' ),
listItem = CKEDITOR.addTemplate( 'panel-list-item', '<li id="{id}" class="cke_panel_listItem" role=presentation>' +
'<a id="{id}_option" _cke_focus=1 hidefocus=true' +
' title="{title}"' +
' href="javascript:void(\'{val}\')" ' +
' {onclick}="CKEDITOR.tools.callFunction({clickFn},\'{val}\'); return false;"' + // #188
' role="option">' +
'{text}' +
'</a>' +
'</li>' ),
listGroup = CKEDITOR.addTemplate( 'panel-list-group', '<h1 id="{id}" class="cke_panel_grouptitle" role="presentation" >{label}</h1>' ),
reSingleQuote = /\'/g,
escapeSingleQuotes = function( str ) {
return str.replace( reSingleQuote, '\\\'' );
};
CKEDITOR.ui.panel.prototype.addListBlock = function( name, definition ) {
return this.addBlock( name, new CKEDITOR.ui.listBlock( this.getHolderElement(), definition ) );
};
CKEDITOR.ui.listBlock = CKEDITOR.tools.createClass( {
base: CKEDITOR.ui.panel.block,
$: function( blockHolder, blockDefinition ) {
blockDefinition = blockDefinition || {};
var attribs = blockDefinition.attributes || ( blockDefinition.attributes = {} );
( this.multiSelect = !!blockDefinition.multiSelect ) && ( attribs[ 'aria-multiselectable' ] = true );
// Provide default role of 'listbox'.
!attribs.role && ( attribs.role = 'listbox' );
// Call the base contructor.
this.base.apply( this, arguments );
// Set the proper a11y attributes.
this.element.setAttribute( 'role', attribs.role );
var keys = this.keys;
keys[ 40 ] = 'next'; // ARROW-DOWN
keys[ 9 ] = 'next'; // TAB
keys[ 38 ] = 'prev'; // ARROW-UP
keys[ CKEDITOR.SHIFT + 9 ] = 'prev'; // SHIFT + TAB
keys[ 32 ] = CKEDITOR.env.ie ? 'mouseup' : 'click'; // SPACE
CKEDITOR.env.ie && ( keys[ 13 ] = 'mouseup' ); // Manage ENTER, since onclick is blocked in IE (#8041).
this._.pendingHtml = [];
this._.pendingList = [];
this._.items = {};
this._.groups = {};
},
_: {
close: function() {
if ( this._.started ) {
var output = list.output( { items: this._.pendingList.join( '' ) } );
this._.pendingList = [];
this._.pendingHtml.push( output );
delete this._.started;
}
},
getClick: function() {
if ( !this._.click ) {
this._.click = CKEDITOR.tools.addFunction( function( value ) {
var marked = this.toggle( value );
if ( this.onClick )
this.onClick( value, marked );
}, this );
}
return this._.click;
}
},
proto: {
add: function( value, html, title ) {
var id = CKEDITOR.tools.getNextId();
if ( !this._.started ) {
this._.started = 1;
this._.size = this._.size || 0;
}
this._.items[ value ] = id;
var data = {
id: id,
val: escapeSingleQuotes( CKEDITOR.tools.htmlEncodeAttr( value ) ),
onclick: CKEDITOR.env.ie ? 'onclick="return false;" onmouseup' : 'onclick',
clickFn: this._.getClick(),
title: CKEDITOR.tools.htmlEncodeAttr( title || value ),
text: html || value
};
this._.pendingList.push( listItem.output( data ) );
},
startGroup: function( title ) {
this._.close();
var id = CKEDITOR.tools.getNextId();
this._.groups[ title ] = id;
this._.pendingHtml.push( listGroup.output( { id: id, label: title } ) );
},
commit: function() {
this._.close();
this.element.appendHtml( this._.pendingHtml.join( '' ) );
delete this._.size;
this._.pendingHtml = [];
},
toggle: function( value ) {
var isMarked = this.isMarked( value );
if ( isMarked )
this.unmark( value );
else
this.mark( value );
return !isMarked;
},
hideGroup: function( groupTitle ) {
var group = this.element.getDocument().getById( this._.groups[ groupTitle ] ),
list = group && group.getNext();
if ( group ) {
group.setStyle( 'display', 'none' );
if ( list && list.getName() == 'ul' )
list.setStyle( 'display', 'none' );
}
},
hideItem: function( value ) {
this.element.getDocument().getById( this._.items[ value ] ).setStyle( 'display', 'none' );
},
showAll: function() {
var items = this._.items,
groups = this._.groups,
doc = this.element.getDocument();
for ( var value in items ) {
doc.getById( items[ value ] ).setStyle( 'display', '' );
}
for ( var title in groups ) {
var group = doc.getById( groups[ title ] ),
list = group.getNext();
group.setStyle( 'display', '' );
if ( list && list.getName() == 'ul' )
list.setStyle( 'display', '' );
}
},
mark: function( value ) {
if ( !this.multiSelect )
this.unmarkAll();
var itemId = this._.items[ value ],
item = this.element.getDocument().getById( itemId );
item.addClass( 'cke_selected' );
this.element.getDocument().getById( itemId + '_option' ).setAttribute( 'aria-selected', true );
this.onMark && this.onMark( item );
},
unmark: function( value ) {
var doc = this.element.getDocument(),
itemId = this._.items[ value ],
item = doc.getById( itemId );
item.removeClass( 'cke_selected' );
doc.getById( itemId + '_option' ).removeAttribute( 'aria-selected' );
this.onUnmark && this.onUnmark( item );
},
unmarkAll: function() {
var items = this._.items,
doc = this.element.getDocument();
for ( var value in items ) {
var itemId = items[ value ];
doc.getById( itemId ).removeClass( 'cke_selected' );
doc.getById( itemId + '_option' ).removeAttribute( 'aria-selected' );
}
this.onUnmark && this.onUnmark();
},
isMarked: function( value ) {
return this.element.getDocument().getById( this._.items[ value ] ).hasClass( 'cke_selected' );
},
focus: function( value ) {
this._.focusIndex = -1;
var links = this.element.getElementsByTag( 'a' ),
link,
selected,
i = -1;
if ( value ) {
selected = this.element.getDocument().getById( this._.items[ value ] ).getFirst();
while ( ( link = links.getItem( ++i ) ) ) {
if ( link.equals( selected ) ) {
this._.focusIndex = i;
break;
}
}
}
else
this.element.focus();
selected && setTimeout( function() {
selected.focus();
}, 0 );
}
}
} );
}
} );

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,305 @@
/**
* @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
( function() {
function protectFormStyles( formElement ) {
if ( !formElement || formElement.type != CKEDITOR.NODE_ELEMENT || formElement.getName() != 'form' )
return [];
var hijackRecord = [],
hijackNames = [ 'style', 'className' ];
for ( var i = 0; i < hijackNames.length; i++ ) {
var name = hijackNames[ i ];
var $node = formElement.$.elements.namedItem( name );
if ( $node ) {
var hijackNode = new CKEDITOR.dom.element( $node );
hijackRecord.push( [ hijackNode, hijackNode.nextSibling ] );
hijackNode.remove();
}
}
return hijackRecord;
}
function restoreFormStyles( formElement, hijackRecord ) {
if ( !formElement || formElement.type != CKEDITOR.NODE_ELEMENT || formElement.getName() != 'form' )
return;
if ( hijackRecord.length > 0 ) {
for ( var i = hijackRecord.length - 1; i >= 0; i-- ) {
var node = hijackRecord[ i ][ 0 ];
var sibling = hijackRecord[ i ][ 1 ];
if ( sibling )
node.insertBefore( sibling );
else
node.appendTo( formElement );
}
}
}
function saveStyles( element, isInsideEditor ) {
var data = protectFormStyles( element );
var retval = {};
var $element = element.$;
if ( !isInsideEditor ) {
retval[ 'class' ] = $element.className || '';
$element.className = '';
}
retval.inline = $element.style.cssText || '';
if ( !isInsideEditor ) // Reset any external styles that might interfere. (#2474)
$element.style.cssText = 'position: static; overflow: visible';
restoreFormStyles( data );
return retval;
}
function restoreStyles( element, savedStyles ) {
var data = protectFormStyles( element );
var $element = element.$;
if ( 'class' in savedStyles )
$element.className = savedStyles[ 'class' ];
if ( 'inline' in savedStyles )
$element.style.cssText = savedStyles.inline;
restoreFormStyles( data );
}
function refreshCursor( editor ) {
if ( editor.editable().isInline() )
return;
// Refresh all editor instances on the page (#5724).
var all = CKEDITOR.instances;
for ( var i in all ) {
var one = all[ i ];
if ( one.mode == 'wysiwyg' && !one.readOnly ) {
var body = one.document.getBody();
// Refresh 'contentEditable' otherwise
// DOM lifting breaks design mode. (#5560)
body.setAttribute( 'contentEditable', false );
body.setAttribute( 'contentEditable', true );
}
}
if ( editor.editable().hasFocus ) {
editor.toolbox.focus();
editor.focus();
}
}
CKEDITOR.plugins.add( 'maximize', {
lang: 'af,ar,bg,bn,bs,ca,cs,cy,da,de,el,en,en-au,en-ca,en-gb,eo,es,et,eu,fa,fi,fo,fr,fr-ca,gl,gu,he,hi,hr,hu,id,is,it,ja,ka,km,ko,ku,lt,lv,mk,mn,ms,nb,nl,no,pl,pt,pt-br,ro,ru,si,sk,sl,sq,sr,sr-latn,sv,th,tr,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE%
icons: 'maximize', // %REMOVE_LINE_CORE%
hidpi: true, // %REMOVE_LINE_CORE%
init: function( editor ) {
// Maximize plugin isn't available in inline mode yet.
if ( editor.elementMode == CKEDITOR.ELEMENT_MODE_INLINE )
return;
var lang = editor.lang;
var mainDocument = CKEDITOR.document,
mainWindow = mainDocument.getWindow();
// Saved selection and scroll position for the editing area.
var savedSelection, savedScroll;
// Saved scroll position for the outer window.
var outerScroll;
// Saved resize handler function.
function resizeHandler() {
var viewPaneSize = mainWindow.getViewPaneSize();
editor.resize( viewPaneSize.width, viewPaneSize.height, null, true );
}
// Retain state after mode switches.
var savedState = CKEDITOR.TRISTATE_OFF;
editor.addCommand( 'maximize', {
// Disabled on iOS (#8307).
modes: { wysiwyg: !CKEDITOR.env.iOS, source: !CKEDITOR.env.iOS },
readOnly: 1,
editorFocus: false,
exec: function() {
var container = editor.container.getChild( 1 );
var contents = editor.ui.space( 'contents' );
// Save current selection and scroll position in editing area.
if ( editor.mode == 'wysiwyg' ) {
var selection = editor.getSelection();
savedSelection = selection && selection.getRanges();
savedScroll = mainWindow.getScrollPosition();
} else {
var $textarea = editor.editable().$;
savedSelection = !CKEDITOR.env.ie && [ $textarea.selectionStart, $textarea.selectionEnd ];
savedScroll = [ $textarea.scrollLeft, $textarea.scrollTop ];
}
if ( this.state == CKEDITOR.TRISTATE_OFF ) // Go fullscreen if the state is off.
{
// Add event handler for resizing.
mainWindow.on( 'resize', resizeHandler );
// Save the scroll bar position.
outerScroll = mainWindow.getScrollPosition();
// Save and reset the styles for the entire node tree.
var currentNode = editor.container;
while ( ( currentNode = currentNode.getParent() ) ) {
currentNode.setCustomData( 'maximize_saved_styles', saveStyles( currentNode ) );
// Show under floatpanels (-1) and context menu (-2).
currentNode.setStyle( 'z-index', editor.config.baseFloatZIndex - 5 );
}
contents.setCustomData( 'maximize_saved_styles', saveStyles( contents, true ) );
container.setCustomData( 'maximize_saved_styles', saveStyles( container, true ) );
// Hide scroll bars.
var styles = {
overflow: CKEDITOR.env.webkit ? '' : 'hidden', // #6896
width: 0,
height: 0
};
mainDocument.getDocumentElement().setStyles( styles );
!CKEDITOR.env.gecko && mainDocument.getDocumentElement().setStyle( 'position', 'fixed' );
!( CKEDITOR.env.gecko && CKEDITOR.env.quirks ) && mainDocument.getBody().setStyles( styles );
// Scroll to the top left (IE needs some time for it - #4923).
CKEDITOR.env.ie ? setTimeout( function() {
mainWindow.$.scrollTo( 0, 0 );
}, 0 ) : mainWindow.$.scrollTo( 0, 0 );
// Resize and move to top left.
// Special treatment for FF Quirks (#7284)
container.setStyle( 'position', CKEDITOR.env.gecko && CKEDITOR.env.quirks ? 'fixed' : 'absolute' );
container.$.offsetLeft; // SAFARI BUG: See #2066.
container.setStyles( {
// Show under floatpanels (-1) and context menu (-2).
'z-index': editor.config.baseFloatZIndex - 5,
left: '0px',
top: '0px'
} );
// Add cke_maximized class before resize handle since that will change things sizes (#5580)
container.addClass( 'cke_maximized' );
resizeHandler();
// Still not top left? Fix it. (Bug #174)
var offset = container.getDocumentPosition();
container.setStyles( {
left: ( -1 * offset.x ) + 'px',
top: ( -1 * offset.y ) + 'px'
} );
// Fixing positioning editor chrome in Firefox break design mode. (#5149)
CKEDITOR.env.gecko && refreshCursor( editor );
} else if ( this.state == CKEDITOR.TRISTATE_ON ) // Restore from fullscreen if the state is on.
{
// Remove event handler for resizing.
mainWindow.removeListener( 'resize', resizeHandler );
// Restore CSS styles for the entire node tree.
var editorElements = [ contents, container ];
for ( var i = 0; i < editorElements.length; i++ ) {
restoreStyles( editorElements[ i ], editorElements[ i ].getCustomData( 'maximize_saved_styles' ) );
editorElements[ i ].removeCustomData( 'maximize_saved_styles' );
}
currentNode = editor.container;
while ( ( currentNode = currentNode.getParent() ) ) {
restoreStyles( currentNode, currentNode.getCustomData( 'maximize_saved_styles' ) );
currentNode.removeCustomData( 'maximize_saved_styles' );
}
// Restore the window scroll position.
CKEDITOR.env.ie ? setTimeout( function() {
mainWindow.$.scrollTo( outerScroll.x, outerScroll.y );
}, 0 ) : mainWindow.$.scrollTo( outerScroll.x, outerScroll.y );
// Remove cke_maximized class.
container.removeClass( 'cke_maximized' );
// Webkit requires a re-layout on editor chrome. (#6695)
if ( CKEDITOR.env.webkit ) {
container.setStyle( 'display', 'inline' );
setTimeout( function() {
container.setStyle( 'display', 'block' );
}, 0 );
}
// Emit a resize event, because this time the size is modified in
// restoreStyles.
editor.fire( 'resize' );
}
this.toggleState();
// Toggle button label.
var button = this.uiItems[ 0 ];
// Only try to change the button if it exists (#6166)
if ( button ) {
var label = ( this.state == CKEDITOR.TRISTATE_OFF ) ? lang.maximize.maximize : lang.maximize.minimize;
var buttonNode = CKEDITOR.document.getById( button._.id );
buttonNode.getChild( 1 ).setHtml( label );
buttonNode.setAttribute( 'title', label );
buttonNode.setAttribute( 'href', 'javascript:void("' + label + '");' );
}
// Restore selection and scroll position in editing area.
if ( editor.mode == 'wysiwyg' ) {
if ( savedSelection ) {
// Fixing positioning editor chrome in Firefox break design mode. (#5149)
CKEDITOR.env.gecko && refreshCursor( editor );
editor.getSelection().selectRanges( savedSelection );
var element = editor.getSelection().getStartElement();
element && element.scrollIntoView( true );
} else
mainWindow.$.scrollTo( savedScroll.x, savedScroll.y );
} else {
if ( savedSelection ) {
$textarea.selectionStart = savedSelection[ 0 ];
$textarea.selectionEnd = savedSelection[ 1 ];
}
$textarea.scrollLeft = savedScroll[ 0 ];
$textarea.scrollTop = savedScroll[ 1 ];
}
savedSelection = savedScroll = null;
savedState = this.state;
editor.fire( 'maximize', this.state );
},
canUndo: false
} );
editor.ui.addButton && editor.ui.addButton( 'Maximize', {
label: lang.maximize.maximize,
command: 'maximize',
toolbar: 'tools,10'
} );
// Restore the command state after mode change, unless it has been changed to disabled (#6467)
editor.on( 'mode', function() {
var command = editor.getCommand( 'maximize' );
command.setState( command.state == CKEDITOR.TRISTATE_DISABLED ? CKEDITOR.TRISTATE_DISABLED : savedState );
}, null, null, 100 );
}
} );
} )();
/**
* Event fired when the maximize command is called.
* It also indicates whether an editor is maximized or not.
*
* @event maximize
* @member CKEDITOR.editor
* @param {CKEDITOR.editor} editor This editor instance.
* @param {Number} data Current state of the command. See {@link CKEDITOR#TRISTATE_ON} and {@link CKEDITOR#TRISTATE_OFF}.
*/

View File

@ -0,0 +1,545 @@
/**
* @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
CKEDITOR.plugins.add( 'menu', {
requires: 'floatpanel',
beforeInit: function( editor ) {
var groups = editor.config.menu_groups.split( ',' ),
groupsOrder = editor._.menuGroups = {},
menuItems = editor._.menuItems = {};
for ( var i = 0; i < groups.length; i++ )
groupsOrder[ groups[ i ] ] = i + 1;
/**
* Registers an item group to the editor context menu in order to make it
* possible to associate it with menu items later.
*
* @param {String} name Specify a group name.
* @param {Number} [order=100] Define the display sequence of this group
* inside the menu. A smaller value gets displayed first.
* @member CKEDITOR.editor
*/
editor.addMenuGroup = function( name, order ) {
groupsOrder[ name ] = order || 100;
};
/**
* Adds an item from the specified definition to the editor context menu.
*
* @method
* @param {String} name The menu item name.
* @param {Object} definition The menu item definition.
* @member CKEDITOR.editor
*/
editor.addMenuItem = function( name, definition ) {
if ( groupsOrder[ definition.group ] )
menuItems[ name ] = new CKEDITOR.menuItem( this, name, definition );
};
/**
* Adds one or more items from the specified definition array to the editor context menu.
*
* @method
* @param {Array} definitions List of definitions for each menu item as if {@link #addMenuItem} is called.
* @member CKEDITOR.editor
*/
editor.addMenuItems = function( definitions ) {
for ( var itemName in definitions ) {
this.addMenuItem( itemName, definitions[ itemName ] );
}
};
/**
* Retrieves a particular menu item definition from the editor context menu.
*
* @method
* @param {String} name The name of the desired menu item.
* @returns {Object}
* @member CKEDITOR.editor
*/
editor.getMenuItem = function( name ) {
return menuItems[ name ];
};
/**
* Removes a particular menu item added before from the editor context menu.
*
* @since 3.6.1
* @method
* @param {String} name The name of the desired menu item.
* @member CKEDITOR.editor
*/
editor.removeMenuItem = function( name ) {
delete menuItems[ name ];
};
}
} );
( function() {
var menuItemSource = '<span class="cke_menuitem">' +
'<a id="{id}"' +
' class="cke_menubutton cke_menubutton__{name} cke_menubutton_{state} {cls}" href="{href}"' +
' title="{title}"' +
' tabindex="-1"' +
'_cke_focus=1' +
' hidefocus="true"' +
' role="{role}"' +
' aria-haspopup="{hasPopup}"' +
' aria-disabled="{disabled}"' +
' {ariaChecked}';
// Some browsers don't cancel key events in the keydown but in the
// keypress.
// TODO: Check if really needed for Gecko+Mac.
if ( CKEDITOR.env.opera || ( CKEDITOR.env.gecko && CKEDITOR.env.mac ) )
menuItemSource += ' onkeypress="return false;"';
// With Firefox, we need to force the button to redraw, otherwise it
// will remain in the focus state.
if ( CKEDITOR.env.gecko )
menuItemSource += ' onblur="this.style.cssText = this.style.cssText;"';
// #188
menuItemSource += ' onmouseover="CKEDITOR.tools.callFunction({hoverFn},{index});"' +
' onmouseout="CKEDITOR.tools.callFunction({moveOutFn},{index});" ' +
( CKEDITOR.env.ie ? 'onclick="return false;" onmouseup' : 'onclick' ) +
'="CKEDITOR.tools.callFunction({clickFn},{index}); return false;"' +
'>';
menuItemSource +=
'<span class="cke_menubutton_inner">' +
'<span class="cke_menubutton_icon">' +
'<span class="cke_button_icon cke_button__{iconName}_icon" style="{iconStyle}"></span>' +
'</span>' +
'<span class="cke_menubutton_label">' +
'{label}' +
'</span>' +
'{arrowHtml}' +
'</span>' +
'</a></span>';
var menuArrowSource = '<span class="cke_menuarrow">' +
'<span>{label}</span>' +
'</span>';
var menuItemTpl = CKEDITOR.addTemplate( 'menuItem', menuItemSource ),
menuArrowTpl = CKEDITOR.addTemplate( 'menuArrow', menuArrowSource );
/**
* @class
* @todo
*/
CKEDITOR.menu = CKEDITOR.tools.createClass( {
/**
* @constructor
*/
$: function( editor, definition ) {
definition = this._.definition = definition || {};
this.id = CKEDITOR.tools.getNextId();
this.editor = editor;
this.items = [];
this._.listeners = [];
this._.level = definition.level || 1;
var panelDefinition = CKEDITOR.tools.extend( {}, definition.panel, {
css: [ CKEDITOR.skin.getPath( 'editor' ) ],
level: this._.level - 1,
block: {}
} );
var attrs = panelDefinition.block.attributes = ( panelDefinition.attributes || {} );
// Provide default role of 'menu'.
!attrs.role && ( attrs.role = 'menu' );
this._.panelDefinition = panelDefinition;
},
_: {
onShow: function() {
var selection = this.editor.getSelection(),
start = selection && selection.getStartElement(),
path = this.editor.elementPath(),
listeners = this._.listeners;
this.removeAll();
// Call all listeners, filling the list of items to be displayed.
for ( var i = 0; i < listeners.length; i++ ) {
var listenerItems = listeners[ i ]( start, selection, path );
if ( listenerItems ) {
for ( var itemName in listenerItems ) {
var item = this.editor.getMenuItem( itemName );
if ( item && ( !item.command || this.editor.getCommand( item.command ).state ) ) {
item.state = listenerItems[ itemName ];
this.add( item );
}
}
}
}
},
onClick: function( item ) {
this.hide();
if ( item.onClick )
item.onClick();
else if ( item.command )
this.editor.execCommand( item.command );
},
onEscape: function( keystroke ) {
var parent = this.parent;
// 1. If it's sub-menu, close it, with focus restored on this.
// 2. In case of a top-menu, close it, with focus returned to page.
if ( parent )
parent._.panel.hideChild( 1 );
else if ( keystroke == 27 )
this.hide( 1 );
return false;
},
onHide: function() {
this.onHide && this.onHide();
},
showSubMenu: function( index ) {
var menu = this._.subMenu,
item = this.items[ index ],
subItemDefs = item.getItems && item.getItems();
// If this item has no subitems, we just hide the submenu, if
// available, and return back.
if ( !subItemDefs ) {
// Hide sub menu with focus returned.
this._.panel.hideChild( 1 );
return;
}
// Create the submenu, if not available, or clean the existing
// one.
if ( menu )
menu.removeAll();
else {
menu = this._.subMenu = new CKEDITOR.menu( this.editor, CKEDITOR.tools.extend( {}, this._.definition, { level: this._.level + 1 }, true ) );
menu.parent = this;
menu._.onClick = CKEDITOR.tools.bind( this._.onClick, this );
}
// Add all submenu items to the menu.
for ( var subItemName in subItemDefs ) {
var subItem = this.editor.getMenuItem( subItemName );
if ( subItem ) {
subItem.state = subItemDefs[ subItemName ];
menu.add( subItem );
}
}
// Get the element representing the current item.
var element = this._.panel.getBlock( this.id ).element.getDocument().getById( this.id + String( index ) );
// Show the submenu.
// This timeout is needed to give time for the sub-menu get
// focus when JAWS is running. (#9844)
setTimeout( function() {
menu.show( element, 2 );
}, 0 );
}
},
proto: {
/**
* Adds an item.
*
* @param item
*/
add: function( item ) {
// Later we may sort the items, but Array#sort is not stable in
// some browsers, here we're forcing the original sequence with
// 'order' attribute if it hasn't been assigned. (#3868)
if ( !item.order )
item.order = this.items.length;
this.items.push( item );
},
/**
* Removes all items.
*/
removeAll: function() {
this.items = [];
},
/**
* Shows the menu in given location.
*
* @param {CKEDITOR.dom.element} offsetParent
* @param {Number} [corner]
* @param {Number} [offsetX]
* @param {Number} [offsetY]
*/
show: function( offsetParent, corner, offsetX, offsetY ) {
// Not for sub menu.
if ( !this.parent ) {
this._.onShow();
// Don't menu with zero items.
if ( !this.items.length )
return;
}
corner = corner || ( this.editor.lang.dir == 'rtl' ? 2 : 1 );
var items = this.items,
editor = this.editor,
panel = this._.panel,
element = this._.element;
// Create the floating panel for this menu.
if ( !panel ) {
panel = this._.panel = new CKEDITOR.ui.floatPanel( this.editor, CKEDITOR.document.getBody(), this._.panelDefinition, this._.level );
panel.onEscape = CKEDITOR.tools.bind( function( keystroke ) {
if ( this._.onEscape( keystroke ) === false )
return false;
}, this );
panel.onShow = function() {
// Menu need CSS resets, compensate class name.
var holder = panel._.panel.getHolderElement();
holder.getParent().addClass( 'cke cke_reset_all' );
};
panel.onHide = CKEDITOR.tools.bind( function() {
this._.onHide && this._.onHide();
}, this );
// Create an autosize block inside the panel.
var block = panel.addBlock( this.id, this._.panelDefinition.block );
block.autoSize = true;
var keys = block.keys;
keys[ 40 ] = 'next'; // ARROW-DOWN
keys[ 9 ] = 'next'; // TAB
keys[ 38 ] = 'prev'; // ARROW-UP
keys[ CKEDITOR.SHIFT + 9 ] = 'prev'; // SHIFT + TAB
keys[ ( editor.lang.dir == 'rtl' ? 37 : 39 ) ] = CKEDITOR.env.ie ? 'mouseup' : 'click'; // ARROW-RIGHT/ARROW-LEFT(rtl)
keys[ 32 ] = CKEDITOR.env.ie ? 'mouseup' : 'click'; // SPACE
CKEDITOR.env.ie && ( keys[ 13 ] = 'mouseup' ); // Manage ENTER, since onclick is blocked in IE (#8041).
element = this._.element = block.element;
var elementDoc = element.getDocument();
elementDoc.getBody().setStyle( 'overflow', 'hidden' );
elementDoc.getElementsByTag( 'html' ).getItem( 0 ).setStyle( 'overflow', 'hidden' );
this._.itemOverFn = CKEDITOR.tools.addFunction( function( index ) {
clearTimeout( this._.showSubTimeout );
this._.showSubTimeout = CKEDITOR.tools.setTimeout( this._.showSubMenu, editor.config.menu_subMenuDelay || 400, this, [ index ] );
}, this );
this._.itemOutFn = CKEDITOR.tools.addFunction( function( index ) {
clearTimeout( this._.showSubTimeout );
}, this );
this._.itemClickFn = CKEDITOR.tools.addFunction( function( index ) {
var item = this.items[ index ];
if ( item.state == CKEDITOR.TRISTATE_DISABLED ) {
this.hide( 1 );
return;
}
if ( item.getItems )
this._.showSubMenu( index );
else
this._.onClick( item );
}, this );
}
// Put the items in the right order.
sortItems( items );
// Apply the editor mixed direction status to menu.
var path = editor.elementPath(),
mixedDirCls = ( path && path.direction() != editor.lang.dir ) ? ' cke_mixed_dir_content' : '';
// Build the HTML that composes the menu and its items.
var output = [ '<div class="cke_menu' + mixedDirCls + '" role="presentation">' ];
var length = items.length,
lastGroup = length && items[ 0 ].group;
for ( var i = 0; i < length; i++ ) {
var item = items[ i ];
if ( lastGroup != item.group ) {
output.push( '<div class="cke_menuseparator" role="separator"></div>' );
lastGroup = item.group;
}
item.render( this, i, output );
}
output.push( '</div>' );
// Inject the HTML inside the panel.
element.setHtml( output.join( '' ) );
CKEDITOR.ui.fire( 'ready', this );
// Show the panel.
if ( this.parent )
this.parent._.panel.showAsChild( panel, this.id, offsetParent, corner, offsetX, offsetY );
else
panel.showBlock( this.id, offsetParent, corner, offsetX, offsetY );
editor.fire( 'menuShow', [ panel ] );
},
/**
* Adds a callback executed on opening the menu. Items
* returned by that callback are added to the menu.
*
* @param {Function} listenerFn
* @param {CKEDITOR.dom.element} listenerFn.startElement The selection start anchor element.
* @param {CKEDITOR.dom.selection} listenerFn.selection The current selection.
* @param {CKEDITOR.dom.elementPath} listenerFn.path The current elements path.
* @param listenerFn.return Object (`commandName` => `state`) of items that should be added to the menu.
*/
addListener: function( listenerFn ) {
this._.listeners.push( listenerFn );
},
/**
* Hides the menu.
*
* @param {Boolean} [returnFocus]
*/
hide: function( returnFocus ) {
this._.onHide && this._.onHide();
this._.panel && this._.panel.hide( returnFocus );
}
}
} );
function sortItems( items ) {
items.sort( function( itemA, itemB ) {
if ( itemA.group < itemB.group )
return -1;
else if ( itemA.group > itemB.group )
return 1;
return itemA.order < itemB.order ? -1 : itemA.order > itemB.order ? 1 : 0;
} );
}
/**
* @class
* @todo
*/
CKEDITOR.menuItem = CKEDITOR.tools.createClass( {
$: function( editor, name, definition ) {
CKEDITOR.tools.extend( this, definition,
// Defaults
{
order: 0,
className: 'cke_menubutton__' + name
} );
// Transform the group name into its order number.
this.group = editor._.menuGroups[ this.group ];
this.editor = editor;
this.name = name;
},
proto: {
render: function( menu, index, output ) {
var id = menu.id + String( index ),
state = ( typeof this.state == 'undefined' ) ? CKEDITOR.TRISTATE_OFF : this.state,
ariaChecked = '';
var stateName = state == CKEDITOR.TRISTATE_ON ? 'on' : state == CKEDITOR.TRISTATE_DISABLED ? 'disabled' : 'off';
if ( this.role in { menuitemcheckbox: 1, menuitemradio: 1 } )
ariaChecked = ' aria-checked="' + ( state == CKEDITOR.TRISTATE_ON ? 'true' : 'false' ) + '"';
var hasSubMenu = this.getItems;
// ltr: BLACK LEFT-POINTING POINTER
// rtl: BLACK RIGHT-POINTING POINTER
var arrowLabel = '&#' + ( this.editor.lang.dir == 'rtl' ? '9668' : '9658' ) + ';';
var iconName = this.name;
if ( this.icon && !( /\./ ).test( this.icon ) )
iconName = this.icon;
var params = {
id: id,
name: this.name,
iconName: iconName,
label: this.label,
cls: this.className || '',
state: stateName,
hasPopup: hasSubMenu ? 'true' : 'false',
disabled: state == CKEDITOR.TRISTATE_DISABLED,
title: this.label,
href: 'javascript:void(\'' + ( this.label || '' ).replace( "'" + '' ) + '\')',
hoverFn: menu._.itemOverFn,
moveOutFn: menu._.itemOutFn,
clickFn: menu._.itemClickFn,
index: index,
iconStyle: CKEDITOR.skin.getIconStyle( iconName, ( this.editor.lang.dir == 'rtl' ), iconName == this.icon ? null : this.icon, this.iconOffset ),
arrowHtml: hasSubMenu ? menuArrowTpl.output( { label : arrowLabel } ) : '',
role: this.role ? this.role : 'menuitem',
ariaChecked: ariaChecked
};
menuItemTpl.output( params, output );
}
}
} );
} )();
/**
* The amount of time, in milliseconds, the editor waits before displaying submenu
* options when moving the mouse over options that contain submenus, like the
* "Cell Properties" entry for tables.
*
* // Remove the submenu delay.
* config.menu_subMenuDelay = 0;
*
* @cfg {Number} [menu_subMenuDelay=400]
* @member CKEDITOR.config
*/
/**
* Fired when a menu is shown.
*
* @event menuShow
* @member CKEDITOR.editor
* @param {CKEDITOR.editor} editor This editor instance.
* @param {CKEDITOR.ui.panel[]} data
*/
/**
* A comma separated list of items group names to be displayed in the context
* menu. The order of items will reflect the order specified in this list if
* no priority was defined in the groups.
*
* config.menu_groups = 'clipboard,table,anchor,link,image';
*
* @cfg {String} [menu_groups=see source]
* @member CKEDITOR.config
*/
CKEDITOR.config.menu_groups = 'clipboard,' +
'form,' +
'tablecell,tablecellproperties,tablerow,tablecolumn,table,' +
'anchor,link,image,flash,' +
'checkbox,radio,textfield,hiddenfield,imagebutton,button,select,textarea,div';

View File

@ -0,0 +1,402 @@
/**
* @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
( function() {
CKEDITOR.plugins.add( 'panel', {
beforeInit: function( editor ) {
editor.ui.addHandler( CKEDITOR.UI_PANEL, CKEDITOR.ui.panel.handler );
}
} );
/**
* Panel UI element.
*
* @readonly
* @property {String} [='panel']
* @member CKEDITOR
*/
CKEDITOR.UI_PANEL = 'panel';
/**
* @class
* @constructor Creates a panel class instance.
* @param {CKEDITOR.dom.document} document
* @param {Object} definition
*/
CKEDITOR.ui.panel = function( document, definition ) {
// Copy all definition properties to this object.
if ( definition )
CKEDITOR.tools.extend( this, definition );
// Set defaults.
CKEDITOR.tools.extend( this, {
className: '',
css: []
} );
this.id = CKEDITOR.tools.getNextId();
this.document = document;
this.isFramed = this.forceIFrame || this.css.length;
this._ = {
blocks: {}
};
};
/**
* Represents panel handler object.
*
* @class
* @singleton
* @extends CKEDITOR.ui.handlerDefinition
*/
CKEDITOR.ui.panel.handler = {
/**
* Transforms a panel definition in a {@link CKEDITOR.ui.panel} instance.
*
* @param {Object} definition
* @returns {CKEDITOR.ui.panel}
*/
create: function( definition ) {
return new CKEDITOR.ui.panel( definition );
}
};
var panelTpl = CKEDITOR.addTemplate( 'panel', '<div lang="{langCode}" id="{id}" dir={dir}' +
' class="cke cke_reset_all {editorId} cke_panel cke_panel {cls} cke_{dir}"' +
' style="z-index:{z-index}" role="presentation">' +
'{frame}' +
'</div>' );
var frameTpl = CKEDITOR.addTemplate( 'panel-frame', '<iframe id="{id}" class="cke_panel_frame" role="presentation" frameborder="0" src="{src}"></iframe>' );
var frameDocTpl = CKEDITOR.addTemplate( 'panel-frame-inner', '<!DOCTYPE html>' +
'<html class="cke_panel_container {env}" dir="{dir}" lang="{langCode}">' +
'<head>{css}</head>' +
'<body class="cke_{dir}"' +
' style="margin:0;padding:0" onload="{onload}"></body>' +
'<\/html>' );
/** @class CKEDITOR.ui.panel */
CKEDITOR.ui.panel.prototype = {
/**
* Renders the combo.
*
* @param {CKEDITOR.editor} editor The editor instance which this button is
* to be used by.
* @param {Array} [output] The output array to which append the HTML relative
* to this button.
*/
render: function( editor, output ) {
this.getHolderElement = function() {
var holder = this._.holder;
if ( !holder ) {
if ( this.isFramed ) {
var iframe = this.document.getById( this.id + '_frame' ),
parentDiv = iframe.getParent(),
doc = iframe.getFrameDocument();
// Make it scrollable on iOS. (#8308)
CKEDITOR.env.iOS && parentDiv.setStyles( {
'overflow': 'scroll',
'-webkit-overflow-scrolling': 'touch'
} );
var onLoad = CKEDITOR.tools.addFunction( CKEDITOR.tools.bind( function( ev ) {
this.isLoaded = true;
if ( this.onLoad )
this.onLoad();
}, this ) );
doc.write( frameDocTpl.output( CKEDITOR.tools.extend( {
css: CKEDITOR.tools.buildStyleHtml( this.css ),
onload: 'window.parent.CKEDITOR.tools.callFunction(' + onLoad + ');'
}, data ) ) );
var win = doc.getWindow();
// Register the CKEDITOR global.
win.$.CKEDITOR = CKEDITOR;
// Arrow keys for scrolling is only preventable with 'keypress' event in Opera (#4534).
doc.on( 'key' + ( CKEDITOR.env.opera ? 'press' : 'down' ), function( evt ) {
var keystroke = evt.data.getKeystroke(),
dir = this.document.getById( this.id ).getAttribute( 'dir' );
// Delegate key processing to block.
if ( this._.onKeyDown && this._.onKeyDown( keystroke ) === false ) {
evt.data.preventDefault();
return;
}
// ESC/ARROW-LEFT(ltr) OR ARROW-RIGHT(rtl)
if ( keystroke == 27 || keystroke == ( dir == 'rtl' ? 39 : 37 ) ) {
if ( this.onEscape && this.onEscape( keystroke ) === false )
evt.data.preventDefault();
}
}, this );
holder = doc.getBody();
holder.unselectable();
CKEDITOR.env.air && CKEDITOR.tools.callFunction( onLoad );
} else
holder = this.document.getById( this.id );
this._.holder = holder;
}
return holder;
};
var data = {
editorId: editor.id,
id: this.id,
langCode: editor.langCode,
dir: editor.lang.dir,
cls: this.className,
frame: '',
env: CKEDITOR.env.cssClass,
'z-index': editor.config.baseFloatZIndex + 1
};
if ( this.isFramed ) {
// With IE, the custom domain has to be taken care at first,
// for other browers, the 'src' attribute should be left empty to
// trigger iframe's 'load' event.
var src =
CKEDITOR.env.air ? 'javascript:void(0)' :
CKEDITOR.env.ie ? 'javascript:void(function(){' + encodeURIComponent(
'document.open();' +
// In IE, the document domain must be set any time we call document.open().
'(' + CKEDITOR.tools.fixDomain + ')();' +
'document.close();'
) + '}())' :
'';
data.frame = frameTpl.output( {
id: this.id + '_frame',
src: src
} );
}
var html = panelTpl.output( data );
if ( output )
output.push( html );
return html;
},
/**
* @todo
*/
addBlock: function( name, block ) {
block = this._.blocks[ name ] = block instanceof CKEDITOR.ui.panel.block ? block : new CKEDITOR.ui.panel.block( this.getHolderElement(), block );
if ( !this._.currentBlock )
this.showBlock( name );
return block;
},
/**
* @todo
*/
getBlock: function( name ) {
return this._.blocks[ name ];
},
/**
* @todo
*/
showBlock: function( name ) {
var blocks = this._.blocks,
block = blocks[ name ],
current = this._.currentBlock;
// ARIA role works better in IE on the body element, while on the iframe
// for FF. (#8864)
var holder = !this.forceIFrame || CKEDITOR.env.ie ? this._.holder : this.document.getById( this.id + '_frame' );
if ( current )
current.hide();
this._.currentBlock = block;
CKEDITOR.fire( 'ariaWidget', holder );
// Reset the focus index, so it will always go into the first one.
block._.focusIndex = -1;
this._.onKeyDown = block.onKeyDown && CKEDITOR.tools.bind( block.onKeyDown, block );
block.show();
return block;
},
/**
* @todo
*/
destroy: function() {
this.element && this.element.remove();
}
};
/**
* @class
*
* @todo class and all methods
*/
CKEDITOR.ui.panel.block = CKEDITOR.tools.createClass( {
/**
* Creates a block class instances.
*
* @constructor
* @todo
*/
$: function( blockHolder, blockDefinition ) {
this.element = blockHolder.append( blockHolder.getDocument().createElement( 'div', {
attributes: {
'tabindex': -1,
'class': 'cke_panel_block'
},
styles: {
display: 'none'
}
} ) );
// Copy all definition properties to this object.
if ( blockDefinition )
CKEDITOR.tools.extend( this, blockDefinition );
// Set the a11y attributes of this element ...
this.element.setAttributes( {
'role': this.attributes.role || 'presentation',
'aria-label': this.attributes[ 'aria-label' ],
'title': this.attributes.title || this.attributes[ 'aria-label' ]
} );
this.keys = {};
this._.focusIndex = -1;
// Disable context menu for panels.
this.element.disableContextMenu();
},
_: {
/**
* Mark the item specified by the index as current activated.
*/
markItem: function( index ) {
if ( index == -1 )
return;
var links = this.element.getElementsByTag( 'a' );
var item = links.getItem( this._.focusIndex = index );
// Safari need focus on the iframe window first(#3389), but we need
// lock the blur to avoid hiding the panel.
if ( CKEDITOR.env.webkit || CKEDITOR.env.opera )
item.getDocument().getWindow().focus();
item.focus();
this.onMark && this.onMark( item );
}
},
proto: {
show: function() {
this.element.setStyle( 'display', '' );
},
hide: function() {
if ( !this.onHide || this.onHide.call( this ) !== true )
this.element.setStyle( 'display', 'none' );
},
onKeyDown: function( keystroke, noCycle ) {
var keyAction = this.keys[ keystroke ];
switch ( keyAction ) {
// Move forward.
case 'next':
var index = this._.focusIndex,
links = this.element.getElementsByTag( 'a' ),
link;
while ( ( link = links.getItem( ++index ) ) ) {
// Move the focus only if the element is marked with
// the _cke_focus and it it's visible (check if it has
// width).
if ( link.getAttribute( '_cke_focus' ) && link.$.offsetWidth ) {
this._.focusIndex = index;
link.focus();
break;
}
}
// If no link was found, cycle and restart from the top. (#11125)
if ( !link && !noCycle ) {
this._.focusIndex = -1;
return this.onKeyDown( keystroke, 1 );
}
return false;
// Move backward.
case 'prev':
index = this._.focusIndex;
links = this.element.getElementsByTag( 'a' );
while ( index > 0 && ( link = links.getItem( --index ) ) ) {
// Move the focus only if the element is marked with
// the _cke_focus and it it's visible (check if it has
// width).
if ( link.getAttribute( '_cke_focus' ) && link.$.offsetWidth ) {
this._.focusIndex = index;
link.focus();
break;
}
// Make sure link is null when the loop ends and nothing was
// found (#11125).
link = null;
}
// If no link was found, cycle and restart from the bottom. (#11125)
if ( !link && !noCycle ) {
this._.focusIndex = links.count();
return this.onKeyDown( keystroke, 1 );
}
return false;
case 'click':
case 'mouseup':
index = this._.focusIndex;
link = index >= 0 && this.element.getElementsByTag( 'a' ).getItem( index );
if ( link )
link.$[ keyAction ] ? link.$[ keyAction ]() : link.$[ 'on' + keyAction ]();
return false;
}
return true;
}
}
} );
} )();
/**
* Fired when a panel is added to the document.
*
* @event ariaWidget
* @member CKEDITOR
* @param {Object} data The element wrapping the panel.
*/

View File

@ -0,0 +1,138 @@
/**
* @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
CKEDITOR.plugins.add( 'panelbutton', {
requires: 'button',
onLoad: function() {
function clickFn( editor ) {
var _ = this._;
if ( _.state == CKEDITOR.TRISTATE_DISABLED )
return;
this.createPanel( editor );
if ( _.on ) {
_.panel.hide();
return;
}
_.panel.showBlock( this._.id, this.document.getById( this._.id ), 4 );
}
/**
* @class
* @extends CKEDITOR.ui.button
* @todo class and methods
*/
CKEDITOR.ui.panelButton = CKEDITOR.tools.createClass( {
base: CKEDITOR.ui.button,
/**
* Creates a panelButton class instance.
*
* @constructor
*/
$: function( definition ) {
// We don't want the panel definition in this object.
var panelDefinition = definition.panel || {};
delete definition.panel;
this.base( definition );
this.document = ( panelDefinition.parent && panelDefinition.parent.getDocument() ) || CKEDITOR.document;
panelDefinition.block = {
attributes: panelDefinition.attributes
};
panelDefinition.toolbarRelated = true;
this.hasArrow = true;
this.click = clickFn;
this._ = {
panelDefinition: panelDefinition
};
},
statics: {
handler: {
create: function( definition ) {
return new CKEDITOR.ui.panelButton( definition );
}
}
},
proto: {
createPanel: function( editor ) {
var _ = this._;
if ( _.panel )
return;
var panelDefinition = this._.panelDefinition,
panelBlockDefinition = this._.panelDefinition.block,
panelParentElement = panelDefinition.parent || CKEDITOR.document.getBody(),
panel = this._.panel = new CKEDITOR.ui.floatPanel( editor, panelParentElement, panelDefinition ),
block = panel.addBlock( _.id, panelBlockDefinition ),
me = this;
panel.onShow = function() {
if ( me.className )
this.element.addClass( me.className + '_panel' );
me.setState( CKEDITOR.TRISTATE_ON );
_.on = 1;
me.editorFocus && editor.focus();
if ( me.onOpen )
me.onOpen();
};
panel.onHide = function( preventOnClose ) {
if ( me.className )
this.element.getFirst().removeClass( me.className + '_panel' );
me.setState( me.modes && me.modes[ editor.mode ] ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED );
_.on = 0;
if ( !preventOnClose && me.onClose )
me.onClose();
};
panel.onEscape = function() {
panel.hide( 1 );
me.document.getById( _.id ).focus();
};
if ( this.onBlock )
this.onBlock( panel, block );
block.onHide = function() {
_.on = 0;
me.setState( CKEDITOR.TRISTATE_OFF );
};
}
}
} );
},
beforeInit: function( editor ) {
editor.ui.addHandler( CKEDITOR.UI_PANELBUTTON, CKEDITOR.ui.panelButton.handler );
}
} );
/**
* Button UI element.
*
* @readonly
* @property {String} [='panelbutton']
* @member CKEDITOR
*/
CKEDITOR.UI_PANELBUTTON = 'panelbutton';

View File

@ -0,0 +1,174 @@
/**
* @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
CKEDITOR.plugins.add( 'removeformat', {
lang: 'af,ar,bg,bn,bs,ca,cs,cy,da,de,el,en,en-au,en-ca,en-gb,eo,es,et,eu,fa,fi,fo,fr,fr-ca,gl,gu,he,hi,hr,hu,id,is,it,ja,ka,km,ko,ku,lt,lv,mk,mn,ms,nb,nl,no,pl,pt,pt-br,ro,ru,si,sk,sl,sq,sr,sr-latn,sv,th,tr,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE%
icons: 'removeformat', // %REMOVE_LINE_CORE%
hidpi: true, // %REMOVE_LINE_CORE%
init: function( editor ) {
editor.addCommand( 'removeFormat', CKEDITOR.plugins.removeformat.commands.removeformat );
editor.ui.addButton && editor.ui.addButton( 'RemoveFormat', {
label: editor.lang.removeformat.toolbar,
command: 'removeFormat',
toolbar: 'cleanup,10'
} );
}
} );
CKEDITOR.plugins.removeformat = {
commands: {
removeformat: {
exec: function( editor ) {
var tagsRegex = editor._.removeFormatRegex || ( editor._.removeFormatRegex = new RegExp( '^(?:' + editor.config.removeFormatTags.replace( /,/g, '|' ) + ')$', 'i' ) );
var removeAttributes = editor._.removeAttributes || ( editor._.removeAttributes = editor.config.removeFormatAttributes.split( ',' ) );
var filter = CKEDITOR.plugins.removeformat.filter;
var ranges = editor.getSelection().getRanges( 1 ),
iterator = ranges.createIterator(),
range;
while ( ( range = iterator.getNextRange() ) ) {
if ( !range.collapsed )
range.enlarge( CKEDITOR.ENLARGE_ELEMENT );
// Bookmark the range so we can re-select it after processing.
var bookmark = range.createBookmark(),
// The style will be applied within the bookmark boundaries.
startNode = bookmark.startNode,
endNode = bookmark.endNode,
currentNode;
// We need to check the selection boundaries (bookmark spans) to break
// the code in a way that we can properly remove partially selected nodes.
// For example, removing a <b> style from
// <b>This is [some text</b> to show <b>the] problem</b>
// ... where [ and ] represent the selection, must result:
// <b>This is </b>[some text to show the]<b> problem</b>
// The strategy is simple, we just break the partial nodes before the
// removal logic, having something that could be represented this way:
// <b>This is </b>[<b>some text</b> to show <b>the</b>]<b> problem</b>
var breakParent = function( node ) {
// Let's start checking the start boundary.
var path = editor.elementPath( node ),
pathElements = path.elements;
for ( var i = 1, pathElement; pathElement = pathElements[ i ]; i++ ) {
if ( pathElement.equals( path.block ) || pathElement.equals( path.blockLimit ) )
break;
// If this element can be removed (even partially).
if ( tagsRegex.test( pathElement.getName() ) && filter( editor, pathElement ) )
node.breakParent( pathElement );
}
};
breakParent( startNode );
if ( endNode ) {
breakParent( endNode );
// Navigate through all nodes between the bookmarks.
currentNode = startNode.getNextSourceNode( true, CKEDITOR.NODE_ELEMENT );
while ( currentNode ) {
// If we have reached the end of the selection, stop looping.
if ( currentNode.equals( endNode ) )
break;
// Cache the next node to be processed. Do it now, because
// currentNode may be removed.
var nextNode = currentNode.getNextSourceNode( false, CKEDITOR.NODE_ELEMENT );
// This node must not be a fake element.
if ( !( currentNode.getName() == 'img' && currentNode.data( 'cke-realelement' ) ) && filter( editor, currentNode ) ) {
// Remove elements nodes that match with this style rules.
if ( tagsRegex.test( currentNode.getName() ) )
currentNode.remove( 1 );
else {
currentNode.removeAttributes( removeAttributes );
editor.fire( 'removeFormatCleanup', currentNode );
}
}
currentNode = nextNode;
}
}
range.moveToBookmark( bookmark );
}
// The selection path may not changed, but we should force a selection
// change event to refresh command states, due to the above attribution change. (#9238)
editor.forceNextSelectionCheck();
editor.getSelection().selectRanges( ranges );
}
}
},
// Perform the remove format filters on the passed element.
// @param {CKEDITOR.editor} editor
// @param {CKEDITOR.dom.element} element
filter: function( editor, element ) {
// If editor#addRemoveFotmatFilter hasn't been executed yet value is not initialized.
var filters = editor._.removeFormatFilters || [];
for ( var i = 0; i < filters.length; i++ ) {
if ( filters[ i ]( element ) === false )
return false;
}
return true;
}
};
/**
* Add to a collection of functions to decide whether a specific
* element should be considered as formatting element and thus
* could be removed during `removeFormat` command.
*
* **Note:** Only available with the existence of `removeformat` plugin.
*
* // Don't remove empty span.
* editor.addRemoveFormatFilter( function( element ) {
* return !( element.is( 'span' ) && CKEDITOR.tools.isEmpty( element.getAttributes() ) );
* } );
*
* @since 3.3
* @member CKEDITOR.editor
* @param {Function} func The function to be called, which will be passed a {CKEDITOR.dom.element} element to test.
*/
CKEDITOR.editor.prototype.addRemoveFormatFilter = function( func ) {
if ( !this._.removeFormatFilters )
this._.removeFormatFilters = [];
this._.removeFormatFilters.push( func );
};
/**
* A comma separated list of elements to be removed when executing the `remove
* format` command. Note that only inline elements are allowed.
*
* @cfg
* @member CKEDITOR.config
*/
CKEDITOR.config.removeFormatTags = 'b,big,code,del,dfn,em,font,i,ins,kbd,q,s,samp,small,span,strike,strong,sub,sup,tt,u,var';
/**
* A comma separated list of elements attributes to be removed when executing
* the `remove format` command.
*
* @cfg
* @member CKEDITOR.config
*/
CKEDITOR.config.removeFormatAttributes = 'class,style,lang,width,height,align,hspace,valign';
/**
* Fired after an element was cleaned by the removeFormat plugin.
*
* @event removeFormatCleanup
* @member CKEDITOR.editor
* @param {CKEDITOR.editor} editor This editor instance.
* @param data
* @param {CKEDITOR.dom.element} data.element The element that was cleaned up.
*/

View File

@ -0,0 +1,441 @@
/**
* @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
CKEDITOR.plugins.add( 'richcombo', {
requires: 'floatpanel,listblock,button',
beforeInit: function( editor ) {
editor.ui.addHandler( CKEDITOR.UI_RICHCOMBO, CKEDITOR.ui.richCombo.handler );
}
} );
( function() {
var template = '<span id="{id}"' +
' class="cke_combo cke_combo__{name} {cls}"' +
' role="presentation">' +
'<span id="{id}_label" class="cke_combo_label">{label}</span>' +
'<a class="cke_combo_button" hidefocus=true title="{title}" tabindex="-1"' +
( CKEDITOR.env.gecko && CKEDITOR.env.version >= 10900 && !CKEDITOR.env.hc ? '' : '" href="javascript:void(\'{titleJs}\')"' ) +
' hidefocus="true"' +
' role="button"' +
' aria-labelledby="{id}_label"' +
' aria-haspopup="true"';
// Some browsers don't cancel key events in the keydown but in the
// keypress.
// TODO: Check if really needed for Gecko+Mac.
if ( CKEDITOR.env.opera || ( CKEDITOR.env.gecko && CKEDITOR.env.mac ) )
template += ' onkeypress="return false;"';
// With Firefox, we need to force the button to redraw, otherwise it
// will remain in the focus state.
if ( CKEDITOR.env.gecko )
template += ' onblur="this.style.cssText = this.style.cssText;"';
template +=
' onkeydown="return CKEDITOR.tools.callFunction({keydownFn},event,this);"' +
' onmousedown="return CKEDITOR.tools.callFunction({mousedownFn},event);" ' +
' onfocus="return CKEDITOR.tools.callFunction({focusFn},event);" ' +
( CKEDITOR.env.ie ? 'onclick="return false;" onmouseup' : 'onclick' ) + // #188
'="CKEDITOR.tools.callFunction({clickFn},this);return false;">' +
'<span id="{id}_text" class="cke_combo_text cke_combo_inlinelabel">{label}</span>' +
'<span class="cke_combo_open">' +
'<span class="cke_combo_arrow">' +
// BLACK DOWN-POINTING TRIANGLE
( CKEDITOR.env.hc ? '&#9660;' : CKEDITOR.env.air ? '&nbsp;' : '' ) +
'</span>' +
'</span>' +
'</a>' +
'</span>';
var rcomboTpl = CKEDITOR.addTemplate( 'combo', template );
/**
* Button UI element.
*
* @readonly
* @property {String} [='richcombo']
* @member CKEDITOR
*/
CKEDITOR.UI_RICHCOMBO = 'richcombo';
/**
* @class
* @todo
*/
CKEDITOR.ui.richCombo = CKEDITOR.tools.createClass( {
$: function( definition ) {
// Copy all definition properties to this object.
CKEDITOR.tools.extend( this, definition,
// Set defaults.
{
// The combo won't participate in toolbar grouping.
canGroup: false,
title: definition.label,
modes: { wysiwyg: 1 },
editorFocus: 1
} );
// We don't want the panel definition in this object.
var panelDefinition = this.panel || {};
delete this.panel;
this.id = CKEDITOR.tools.getNextNumber();
this.document = ( panelDefinition.parent && panelDefinition.parent.getDocument() ) || CKEDITOR.document;
panelDefinition.className = 'cke_combopanel';
panelDefinition.block = {
multiSelect: panelDefinition.multiSelect,
attributes: panelDefinition.attributes
};
panelDefinition.toolbarRelated = true;
this._ = {
panelDefinition: panelDefinition,
items: {}
};
},
proto: {
renderHtml: function( editor ) {
var output = [];
this.render( editor, output );
return output.join( '' );
},
/**
* Renders the combo.
*
* @param {CKEDITOR.editor} editor The editor instance which this button is
* to be used by.
* @param {Array} output The output array to which append the HTML relative
* to this button.
*/
render: function( editor, output ) {
var env = CKEDITOR.env;
var id = 'cke_' + this.id;
var clickFn = CKEDITOR.tools.addFunction( function( el ) {
// Restore locked selection in Opera.
if ( selLocked ) {
editor.unlockSelection( 1 );
selLocked = 0;
}
instance.execute( el );
}, this );
var combo = this;
var instance = {
id: id,
combo: this,
focus: function() {
var element = CKEDITOR.document.getById( id ).getChild( 1 );
element.focus();
},
execute: function( el ) {
var _ = combo._;
if ( _.state == CKEDITOR.TRISTATE_DISABLED )
return;
combo.createPanel( editor );
if ( _.on ) {
_.panel.hide();
return;
}
combo.commit();
var value = combo.getValue();
if ( value )
_.list.mark( value );
else
_.list.unmarkAll();
_.panel.showBlock( combo.id, new CKEDITOR.dom.element( el ), 4 );
},
clickFn: clickFn
};
function updateState() {
var state = this.modes[ editor.mode ] ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED;
if ( editor.readOnly && !this.readOnly )
state = CKEDITOR.TRISTATE_DISABLED;
this.setState( state );
this.setValue( '' );
// Let plugin to disable button.
if ( state != CKEDITOR.TRISTATE_DISABLED && this.refresh )
this.refresh();
}
// Update status when activeFilter, mode, selection or readOnly changes.
editor.on( 'activeFilterChange', updateState, this );
editor.on( 'mode', updateState, this );
editor.on( 'selectionChange', updateState, this );
// If this combo is sensitive to readOnly state, update it accordingly.
!this.readOnly && editor.on( 'readOnly', updateState, this );
var keyDownFn = CKEDITOR.tools.addFunction( function( ev, element ) {
ev = new CKEDITOR.dom.event( ev );
var keystroke = ev.getKeystroke();
// ARROW-DOWN
// This call is duplicated in plugins/toolbar/plugin.js in itemKeystroke().
// Move focus to the first element after drop down was opened by the arrow down key.
if ( keystroke == 40 ) {
editor.once( 'panelShow', function( evt ) {
evt.data._.panel._.currentBlock.onKeyDown( 40 );
} );
}
switch ( keystroke ) {
case 13: // ENTER
case 32: // SPACE
case 40: // ARROW-DOWN
// Show panel
CKEDITOR.tools.callFunction( clickFn, element );
break;
default:
// Delegate the default behavior to toolbar button key handling.
instance.onkey( instance, keystroke );
}
// Avoid subsequent focus grab on editor document.
ev.preventDefault();
} );
var focusFn = CKEDITOR.tools.addFunction( function() {
instance.onfocus && instance.onfocus();
} );
var selLocked = 0;
var mouseDownFn = CKEDITOR.tools.addFunction( function() {
// Opera: lock to prevent loosing editable text selection when clicking on button.
if ( CKEDITOR.env.opera ) {
var edt = editor.editable();
if ( edt.isInline() && edt.hasFocus ) {
editor.lockSelection();
selLocked = 1;
}
}
} );
// For clean up
instance.keyDownFn = keyDownFn;
var params = {
id: id,
name: this.name || this.command,
label: this.label,
title: this.title,
cls: this.className || '',
titleJs: env.gecko && env.version >= 10900 && !env.hc ? '' : ( this.title || '' ).replace( "'", '' ),
keydownFn: keyDownFn,
mousedownFn: mouseDownFn,
focusFn: focusFn,
clickFn: clickFn
};
rcomboTpl.output( params, output );
if ( this.onRender )
this.onRender();
return instance;
},
createPanel: function( editor ) {
if ( this._.panel )
return;
var panelDefinition = this._.panelDefinition,
panelBlockDefinition = this._.panelDefinition.block,
panelParentElement = panelDefinition.parent || CKEDITOR.document.getBody(),
namedPanelCls = 'cke_combopanel__' + this.name,
panel = new CKEDITOR.ui.floatPanel( editor, panelParentElement, panelDefinition ),
list = panel.addListBlock( this.id, panelBlockDefinition ),
me = this;
panel.onShow = function() {
this.element.addClass( namedPanelCls );
me.setState( CKEDITOR.TRISTATE_ON );
me._.on = 1;
me.editorFocus && !editor.focusManager.hasFocus && editor.focus();
if ( me.onOpen )
me.onOpen();
// The "panelShow" event is fired assinchronously, after the
// onShow method call.
editor.once( 'panelShow', function() {
list.focus( !list.multiSelect && me.getValue() );
} );
};
panel.onHide = function( preventOnClose ) {
this.element.removeClass( namedPanelCls );
me.setState( me.modes && me.modes[ editor.mode ] ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED );
me._.on = 0;
if ( !preventOnClose && me.onClose )
me.onClose();
};
panel.onEscape = function() {
// Hide drop-down with focus returned.
panel.hide( 1 );
};
list.onClick = function( value, marked ) {
if ( me.onClick )
me.onClick.call( me, value, marked );
panel.hide();
};
this._.panel = panel;
this._.list = list;
panel.getBlock( this.id ).onHide = function() {
me._.on = 0;
me.setState( CKEDITOR.TRISTATE_OFF );
};
if ( this.init )
this.init();
},
setValue: function( value, text ) {
this._.value = value;
var textElement = this.document.getById( 'cke_' + this.id + '_text' );
if ( textElement ) {
if ( !( value || text ) ) {
text = this.label;
textElement.addClass( 'cke_combo_inlinelabel' );
} else
textElement.removeClass( 'cke_combo_inlinelabel' );
textElement.setText( typeof text != 'undefined' ? text : value );
}
},
getValue: function() {
return this._.value || '';
},
unmarkAll: function() {
this._.list.unmarkAll();
},
mark: function( value ) {
this._.list.mark( value );
},
hideItem: function( value ) {
this._.list.hideItem( value );
},
hideGroup: function( groupTitle ) {
this._.list.hideGroup( groupTitle );
},
showAll: function() {
this._.list.showAll();
},
add: function( value, html, text ) {
this._.items[ value ] = text || value;
this._.list.add( value, html, text );
},
startGroup: function( title ) {
this._.list.startGroup( title );
},
commit: function() {
if ( !this._.committed ) {
this._.list.commit();
this._.committed = 1;
CKEDITOR.ui.fire( 'ready', this );
}
this._.committed = 1;
},
setState: function( state ) {
if ( this._.state == state )
return;
var el = this.document.getById( 'cke_' + this.id );
el.setState( state, 'cke_combo' );
state == CKEDITOR.TRISTATE_DISABLED ?
el.setAttribute( 'aria-disabled', true ) :
el.removeAttribute( 'aria-disabled' );
this._.state = state;
},
getState: function() {
return this._.state;
},
enable: function() {
if ( this._.state == CKEDITOR.TRISTATE_DISABLED )
this.setState( this._.lastState );
},
disable: function() {
if ( this._.state != CKEDITOR.TRISTATE_DISABLED ) {
this._.lastState = this._.state;
this.setState( CKEDITOR.TRISTATE_DISABLED );
}
}
},
/**
* Represents richCombo handler object.
*
* @class CKEDITOR.ui.richCombo.handler
* @singleton
* @extends CKEDITOR.ui.handlerDefinition
*/
statics: {
handler: {
/**
* Transforms a richCombo definition in a {@link CKEDITOR.ui.richCombo} instance.
*
* @param {Object} definition
* @returns {CKEDITOR.ui.richCombo}
*/
create: function( definition ) {
return new CKEDITOR.ui.richCombo( definition );
}
}
}
} );
/**
* @param {String} name
* @param {Object} definition
* @member CKEDITOR.ui
* @todo
*/
CKEDITOR.ui.prototype.addRichCombo = function( name, definition ) {
this.add( name, CKEDITOR.UI_RICHCOMBO, definition );
};
} )();

View File

@ -0,0 +1,174 @@
/**
* @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
/**
* @fileOverview The "show border" plugin. The command display visible outline
* border line around all table elements if table doesn't have a none-zero 'border' attribute specified.
*/
( function() {
var commandDefinition = {
preserveState: true,
editorFocus: false,
readOnly: 1,
exec: function( editor ) {
this.toggleState();
this.refresh( editor );
},
refresh: function( editor ) {
if ( editor.document ) {
var funcName = ( this.state == CKEDITOR.TRISTATE_ON ) ? 'attachClass' : 'removeClass';
editor.editable()[ funcName ]( 'cke_show_borders' );
}
}
};
var showBorderClassName = 'cke_show_border';
CKEDITOR.plugins.add( 'showborders', {
modes: { 'wysiwyg': 1 },
onLoad: function() {
var cssStyleText,
cssTemplate =
// TODO: For IE6, we don't have child selector support,
// where nested table cells could be incorrect.
( CKEDITOR.env.ie6Compat ? [
'.%1 table.%2,',
'.%1 table.%2 td, .%1 table.%2 th',
'{',
'border : #d3d3d3 1px dotted',
'}'
] : [
'.%1 table.%2,',
'.%1 table.%2 > tr > td, .%1 table.%2 > tr > th,',
'.%1 table.%2 > tbody > tr > td, .%1 table.%2 > tbody > tr > th,',
'.%1 table.%2 > thead > tr > td, .%1 table.%2 > thead > tr > th,',
'.%1 table.%2 > tfoot > tr > td, .%1 table.%2 > tfoot > tr > th',
'{',
'border : #d3d3d3 1px dotted',
'}'
] ).join( '' );
cssStyleText = cssTemplate.replace( /%2/g, showBorderClassName ).replace( /%1/g, 'cke_show_borders ' );
CKEDITOR.addCss( cssStyleText );
},
init: function( editor ) {
var command = editor.addCommand( 'showborders', commandDefinition );
command.canUndo = false;
if ( editor.config.startupShowBorders !== false )
command.setState( CKEDITOR.TRISTATE_ON );
// Refresh the command on setData.
editor.on( 'mode', function() {
if ( command.state != CKEDITOR.TRISTATE_DISABLED )
command.refresh( editor );
}, null, null, 100 );
// Refresh the command on wysiwyg frame reloads.
editor.on( 'contentDom', function() {
if ( command.state != CKEDITOR.TRISTATE_DISABLED )
command.refresh( editor );
} );
editor.on( 'removeFormatCleanup', function( evt ) {
var element = evt.data;
if ( editor.getCommand( 'showborders' ).state == CKEDITOR.TRISTATE_ON && element.is( 'table' ) && ( !element.hasAttribute( 'border' ) || parseInt( element.getAttribute( 'border' ), 10 ) <= 0 ) )
element.addClass( showBorderClassName );
} );
},
afterInit: function( editor ) {
var dataProcessor = editor.dataProcessor,
dataFilter = dataProcessor && dataProcessor.dataFilter,
htmlFilter = dataProcessor && dataProcessor.htmlFilter;
if ( dataFilter ) {
dataFilter.addRules( {
elements: {
'table': function( element ) {
var attributes = element.attributes,
cssClass = attributes[ 'class' ],
border = parseInt( attributes.border, 10 );
if ( ( !border || border <= 0 ) && ( !cssClass || cssClass.indexOf( showBorderClassName ) == -1 ) )
attributes[ 'class' ] = ( cssClass || '' ) + ' ' + showBorderClassName;
}
}
} );
}
if ( htmlFilter ) {
htmlFilter.addRules( {
elements: {
'table': function( table ) {
var attributes = table.attributes,
cssClass = attributes[ 'class' ];
cssClass && ( attributes[ 'class' ] = cssClass.replace( showBorderClassName, '' ).replace( /\s{2}/, ' ' ).replace( /^\s+|\s+$/, '' ) );
}
}
} );
}
}
} );
// Table dialog must be aware of it.
CKEDITOR.on( 'dialogDefinition', function( ev ) {
var dialogName = ev.data.name;
if ( dialogName == 'table' || dialogName == 'tableProperties' ) {
var dialogDefinition = ev.data.definition,
infoTab = dialogDefinition.getContents( 'info' ),
borderField = infoTab.get( 'txtBorder' ),
originalCommit = borderField.commit;
borderField.commit = CKEDITOR.tools.override( originalCommit, function( org ) {
return function( data, selectedTable ) {
org.apply( this, arguments );
var value = parseInt( this.getValue(), 10 );
selectedTable[ ( !value || value <= 0 ) ? 'addClass' : 'removeClass' ]( showBorderClassName );
};
} );
var advTab = dialogDefinition.getContents( 'advanced' ),
classField = advTab && advTab.get( 'advCSSClasses' );
if ( classField ) {
classField.setup = CKEDITOR.tools.override( classField.setup, function( originalSetup ) {
return function() {
originalSetup.apply( this, arguments );
this.setValue( this.getValue().replace( /cke_show_border/, '' ) );
};
} );
classField.commit = CKEDITOR.tools.override( classField.commit, function( originalCommit ) {
return function( data, element ) {
originalCommit.apply( this, arguments );
if ( !parseInt( element.getAttribute( 'border' ), 10 ) )
element.addClass( 'cke_show_border' );
};
} );
}
}
} );
} )();
/**
* Whether to automatically enable the "show borders" command when the editor loads.
*
* config.startupShowBorders = false;
*
* @cfg {Boolean} [startupShowBorders=true]
* @member CKEDITOR.config
*/

View File

@ -0,0 +1,154 @@
/**
* @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
/**
* @fileOverview The "sourcearea" plugin. It registers the "source" editing
* mode, which displays the raw data being edited in the editor.
*/
( function() {
CKEDITOR.plugins.add( 'sourcearea', {
lang: 'af,ar,bg,bn,bs,ca,cs,cy,da,de,el,en,en-au,en-ca,en-gb,eo,es,et,eu,fa,fi,fo,fr,fr-ca,gl,gu,he,hi,hr,hu,id,is,it,ja,ka,km,ko,ku,lt,lv,mk,mn,ms,nb,nl,no,pl,pt,pt-br,ro,ru,si,sk,sl,sq,sr,sr-latn,sv,th,tr,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE%
icons: 'source,source-rtl', // %REMOVE_LINE_CORE%
hidpi: true, // %REMOVE_LINE_CORE%
init: function( editor ) {
// Source mode isn't available in inline mode yet.
if ( editor.elementMode == CKEDITOR.ELEMENT_MODE_INLINE )
return;
var sourcearea = CKEDITOR.plugins.sourcearea;
editor.addMode( 'source', function( callback ) {
var contentsSpace = editor.ui.space( 'contents' ),
textarea = contentsSpace.getDocument().createElement( 'textarea' );
textarea.setStyles(
CKEDITOR.tools.extend( {
// IE7 has overflow the <textarea> from wrapping table cell.
width: CKEDITOR.env.ie7Compat ? '99%' : '100%',
height: '100%',
resize: 'none',
outline: 'none',
'text-align': 'left'
},
CKEDITOR.tools.cssVendorPrefix( 'tab-size', editor.config.sourceAreaTabSize || 4 ) ) );
// Make sure that source code is always displayed LTR,
// regardless of editor language (#10105).
textarea.setAttribute( 'dir', 'ltr' );
textarea.addClass( 'cke_source cke_reset cke_enable_context_menu' );
editor.ui.space( 'contents' ).append( textarea );
var editable = editor.editable( new sourceEditable( editor, textarea ) );
// Fill the textarea with the current editor data.
editable.setData( editor.getData( 1 ) );
// Having to make <textarea> fixed sized to conquer the following bugs:
// 1. The textarea height/width='100%' doesn't constraint to the 'td' in IE6/7.
// 2. Unexpected vertical-scrolling behavior happens whenever focus is moving out of editor
// if text content within it has overflowed. (#4762)
if ( CKEDITOR.env.ie ) {
editable.attachListener( editor, 'resize', onResize, editable );
editable.attachListener( CKEDITOR.document.getWindow(), 'resize', onResize, editable );
CKEDITOR.tools.setTimeout( onResize, 0, editable );
}
editor.fire( 'ariaWidget', this );
callback();
} );
editor.addCommand( 'source', sourcearea.commands.source );
if ( editor.ui.addButton ) {
editor.ui.addButton( 'Source', {
label: editor.lang.sourcearea.toolbar,
command: 'source',
toolbar: 'mode,10'
} );
}
editor.on( 'mode', function() {
editor.getCommand( 'source' ).setState( editor.mode == 'source' ? CKEDITOR.TRISTATE_ON : CKEDITOR.TRISTATE_OFF );
} );
function onResize() {
// Holder rectange size is stretched by textarea,
// so hide it just for a moment.
this.hide();
this.setStyle( 'height', this.getParent().$.clientHeight + 'px' );
this.setStyle( 'width', this.getParent().$.clientWidth + 'px' );
// When we have proper holder size, show textarea again.
this.show();
}
}
} );
var sourceEditable = CKEDITOR.tools.createClass( {
base: CKEDITOR.editable,
proto: {
setData: function( data ) {
this.setValue( data );
this.editor.fire( 'dataReady' );
},
getData: function() {
return this.getValue();
},
// Insertions are not supported in source editable.
insertHtml: function() {},
insertElement: function() {},
insertText: function() {},
// Read-only support for textarea.
setReadOnly: function( isReadOnly ) {
this[ ( isReadOnly ? 'set' : 'remove' ) + 'Attribute' ]( 'readOnly', 'readonly' );
},
detach: function() {
sourceEditable.baseProto.detach.call( this );
this.clearCustomData();
this.remove();
}
}
} );
} )();
CKEDITOR.plugins.sourcearea = {
commands: {
source: {
modes: { wysiwyg: 1, source: 1 },
editorFocus: false,
readOnly: 1,
exec: function( editor ) {
if ( editor.mode == 'wysiwyg' )
editor.fire( 'saveSnapshot' );
editor.getCommand( 'source' ).setState( CKEDITOR.TRISTATE_DISABLED );
editor.setMode( editor.mode == 'source' ? 'wysiwyg' : 'source' );
},
canUndo: false
}
}
};
/**
* Controls CSS tab-size property of the sourcearea view.
*
* **Note:** Works only with {@link #dataIndentationChars}
* set to `'\t'`. Please consider that not all browsers support CSS
* `tab-size` property yet.
*
* // Set tab-size to 20 characters.
* CKEDITOR.config.sourceAreaTabSize = 20;
*
* @cfg {Number} [sourceAreaTabSize=4]
* @member CKEDITOR.config
* @see CKEDITOR.config#dataIndentationChars
*/

View File

@ -0,0 +1,72 @@
/**
* @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
/**
* @fileOverview Special Character plugin
*/
CKEDITOR.plugins.add( 'specialchar', {
// List of available localizations.
availableLangs: { ar: 1, bg: 1, ca: 1, cs: 1, cy: 1, de: 1, el: 1, en: 1, eo: 1, es: 1, et: 1, fa: 1, fi: 1, fr: 1,
'fr-ca': 1, gl: 1, he: 1, hr: 1, hu: 1, id: 1, it: 1, ja: 1, km: 1, ku: 1, lv: 1, nb: 1, nl: 1, no: 1, pl: 1,
pt: 1, 'pt-br': 1, ru: 1, si: 1, sk: 1, sl: 1, sq: 1, sv: 1, th: 1, tr: 1, ug: 1, uk: 1, vi: 1, zh: 1, 'zh-cn': 1 },
lang: 'af,ar,bg,bn,bs,ca,cs,cy,da,de,el,en,en-au,en-ca,en-gb,eo,es,et,eu,fa,fi,fo,fr,fr-ca,gl,gu,he,hi,hr,hu,id,is,it,ja,ka,km,ko,ku,lt,lv,mk,mn,ms,nb,nl,no,pl,pt,pt-br,ro,ru,si,sk,sl,sq,sr,sr-latn,sv,th,tr,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE%
requires: 'dialog',
icons: 'specialchar', // %REMOVE_LINE_CORE%
hidpi: true, // %REMOVE_LINE_CORE%
init: function( editor ) {
var pluginName = 'specialchar',
plugin = this;
// Register the dialog.
CKEDITOR.dialog.add( pluginName, this.path + 'dialogs/specialchar.js' );
editor.addCommand( pluginName, {
exec: function() {
var langCode = editor.langCode;
langCode =
plugin.availableLangs[ langCode ] ? langCode :
plugin.availableLangs[ langCode.replace( /-.*/, '' ) ] ? langCode.replace( /-.*/, '' ) :
'en';
CKEDITOR.scriptLoader.load( CKEDITOR.getUrl( plugin.path + 'dialogs/lang/' + langCode + '.js' ), function() {
CKEDITOR.tools.extend( editor.lang.specialchar, plugin.langEntries[ langCode ] );
editor.openDialog( pluginName );
} );
},
modes: { wysiwyg: 1 },
canUndo: false
} );
// Register the toolbar button.
editor.ui.addButton && editor.ui.addButton( 'SpecialChar', {
label: editor.lang.specialchar.toolbar,
command: pluginName,
toolbar: 'insert,50'
} );
}
} );
/**
* The list of special characters visible in the "Special Character" dialog window.
*
* config.specialChars = [ '&quot;', '&rsquo;', [ '&custom;', 'Custom label' ] ];
* config.specialChars = config.specialChars.concat( [ '&quot;', [ '&rsquo;', 'Custom label' ] ] );
*
* @cfg
* @member CKEDITOR.config
*/
CKEDITOR.config.specialChars = [
'!', '&quot;', '#', '$', '%', '&amp;', "'", '(', ')', '*', '+', '-', '.', '/',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';',
'&lt;', '=', '&gt;', '?', '@',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
'[', ']', '^', '_', '`',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p',
'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
'{', '|', '}', '~',
"&euro;", "&lsquo;", "&rsquo;", "&ldquo;", "&rdquo;", "&ndash;", "&mdash;", "&iexcl;", "&cent;", "&pound;", "&curren;", "&yen;", "&brvbar;", "&sect;", "&uml;", "&copy;", "&ordf;", "&laquo;", "&not;", "&reg;", "&macr;", "&deg;", "&sup2;", "&sup3;", "&acute;", "&micro;", "&para;", "&middot;", "&cedil;", "&sup1;", "&ordm;", "&raquo;", "&frac14;", "&frac12;", "&frac34;", "&iquest;", "&Agrave;", "&Aacute;", "&Acirc;", "&Atilde;", "&Auml;", "&Aring;", "&AElig;", "&Ccedil;", "&Egrave;", "&Eacute;", "&Ecirc;", "&Euml;", "&Igrave;", "&Iacute;", "&Icirc;", "&Iuml;", "&ETH;", "&Ntilde;", "&Ograve;", "&Oacute;", "&Ocirc;", "&Otilde;", "&Ouml;", "&times;", "&Oslash;", "&Ugrave;", "&Uacute;", "&Ucirc;", "&Uuml;", "&Yacute;", "&THORN;", "&szlig;", "&agrave;", "&aacute;", "&acirc;", "&atilde;", "&auml;", "&aring;", "&aelig;", "&ccedil;", "&egrave;", "&eacute;", "&ecirc;", "&euml;", "&igrave;", "&iacute;", "&icirc;", "&iuml;", "&eth;", "&ntilde;", "&ograve;", "&oacute;", "&ocirc;", "&otilde;", "&ouml;", "&divide;", "&oslash;", "&ugrave;", "&uacute;", "&ucirc;", "&uuml;", "&yacute;", "&thorn;", "&yuml;", "&OElig;", "&oelig;", "&#372;", "&#374", "&#373", "&#375;", "&sbquo;", "&#8219;", "&bdquo;", "&hellip;", "&trade;", "&#9658;", "&bull;", "&rarr;", "&rArr;", "&hArr;", "&diams;", "&asymp;"
];

View File

@ -0,0 +1,188 @@
/**
* @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
( function() {
'use strict';
CKEDITOR.plugins.add( 'stylescombo', {
requires: 'richcombo',
lang: 'af,ar,bg,bn,bs,ca,cs,cy,da,de,el,en,en-au,en-ca,en-gb,eo,es,et,eu,fa,fi,fo,fr,fr-ca,gl,gu,he,hi,hr,hu,id,is,it,ja,ka,km,ko,ku,lt,lv,mk,mn,ms,nb,nl,no,pl,pt,pt-br,ro,ru,si,sk,sl,sq,sr,sr-latn,sv,th,tr,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE%
init: function( editor ) {
var config = editor.config,
lang = editor.lang.stylescombo,
styles = {},
stylesList = [],
combo,
allowedContent = [];
editor.on( 'stylesSet', function( evt ) {
var stylesDefinitions = evt.data.styles;
if ( !stylesDefinitions )
return;
var style, styleName;
// Put all styles into an Array.
for ( var i = 0, count = stylesDefinitions.length; i < count; i++ ) {
var styleDefinition = stylesDefinitions[ i ];
if ( editor.blockless && ( styleDefinition.element in CKEDITOR.dtd.$block ) )
continue;
styleName = styleDefinition.name;
style = new CKEDITOR.style( styleDefinition );
if ( !editor.filter.customConfig || editor.filter.check( style ) ) {
style._name = styleName;
style._.enterMode = config.enterMode;
// Weight is used to sort styles (#9029).
style._.weight = i + ( style.type == CKEDITOR.STYLE_OBJECT ? 1 : style.type == CKEDITOR.STYLE_BLOCK ? 2 : 3 ) * 1000;
styles[ styleName ] = style;
stylesList.push( style );
allowedContent.push( style );
}
}
// Sorts the Array, so the styles get grouped by type in proper order (#9029).
stylesList.sort( function( styleA, styleB ) { return styleA._.weight - styleB._.weight; } );
} );
editor.ui.addRichCombo( 'Styles', {
label: lang.label,
title: lang.panelTitle,
toolbar: 'styles,10',
allowedContent: allowedContent,
panel: {
css: [ CKEDITOR.skin.getPath( 'editor' ) ].concat( config.contentsCss ),
multiSelect: true,
attributes: { 'aria-label': lang.panelTitle }
},
init: function() {
var style, styleName, lastType, type, i, count;
// Loop over the Array, adding all items to the
// combo.
for ( i = 0, count = stylesList.length; i < count; i++ ) {
style = stylesList[ i ];
styleName = style._name;
type = style.type;
if ( type != lastType ) {
this.startGroup( lang[ 'panelTitle' + String( type ) ] );
lastType = type;
}
this.add( styleName, style.type == CKEDITOR.STYLE_OBJECT ? styleName : style.buildPreview(), styleName );
}
this.commit();
},
onClick: function( value ) {
editor.focus();
editor.fire( 'saveSnapshot' );
var style = styles[ value ],
elementPath = editor.elementPath();
editor[ style.checkActive( elementPath ) ? 'removeStyle' : 'applyStyle' ]( style );
editor.fire( 'saveSnapshot' );
},
onRender: function() {
editor.on( 'selectionChange', function( ev ) {
var currentValue = this.getValue(),
elementPath = ev.data.path,
elements = elementPath.elements;
// For each element into the elements path.
for ( var i = 0, count = elements.length, element; i < count; i++ ) {
element = elements[ i ];
// Check if the element is removable by any of
// the styles.
for ( var value in styles ) {
if ( styles[ value ].checkElementRemovable( element, true ) ) {
if ( value != currentValue )
this.setValue( value );
return;
}
}
}
// If no styles match, just empty it.
this.setValue( '' );
}, this );
},
onOpen: function() {
var selection = editor.getSelection(),
element = selection.getSelectedElement(),
elementPath = editor.elementPath( element ),
counter = [ 0, 0, 0, 0 ];
this.showAll();
this.unmarkAll();
for ( var name in styles ) {
var style = styles[ name ],
type = style.type;
if ( style.checkApplicable( elementPath, editor.activeFilter ) )
counter[ type ]++;
else
this.hideItem( name );
if ( style.checkActive( elementPath ) )
this.mark( name );
}
if ( !counter[ CKEDITOR.STYLE_BLOCK ] )
this.hideGroup( lang[ 'panelTitle' + String( CKEDITOR.STYLE_BLOCK ) ] );
if ( !counter[ CKEDITOR.STYLE_INLINE ] )
this.hideGroup( lang[ 'panelTitle' + String( CKEDITOR.STYLE_INLINE ) ] );
if ( !counter[ CKEDITOR.STYLE_OBJECT ] )
this.hideGroup( lang[ 'panelTitle' + String( CKEDITOR.STYLE_OBJECT ) ] );
},
refresh: function() {
var elementPath = editor.elementPath();
if ( !elementPath )
return;
for ( var name in styles ) {
var style = styles[ name ];
if ( style.checkApplicable( elementPath, editor.activeFilter ) )
return;
}
this.setState( CKEDITOR.TRISTATE_DISABLED );
},
// Force a reload of the data
reset: function() {
if ( combo ) {
delete combo._.panel;
delete combo._.list;
combo._.committed = 0;
combo._.items = {};
combo._.state = CKEDITOR.TRISTATE_OFF;
}
styles = {};
stylesList = [];
}
} );
}
} );
} )();

View File

@ -0,0 +1,301 @@
/**
* @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
( function() {
var meta = {
editorFocus: false,
modes: { wysiwyg: 1, source: 1 }
};
var blurCommand = {
exec: function( editor ) {
editor.container.focusNext( true, editor.tabIndex );
}
};
var blurBackCommand = {
exec: function( editor ) {
editor.container.focusPrevious( true, editor.tabIndex );
}
};
function selectNextCellCommand( backward ) {
return {
editorFocus: false,
canUndo: false,
modes: { wysiwyg: 1 },
exec: function( editor ) {
if ( editor.editable().hasFocus ) {
var sel = editor.getSelection(),
path = new CKEDITOR.dom.elementPath( sel.getCommonAncestor(), sel.root ),
cell;
if ( ( cell = path.contains( { td: 1, th: 1 }, 1 ) ) ) {
var resultRange = editor.createRange(),
next = CKEDITOR.tools.tryThese( function() {
var row = cell.getParent(),
next = row.$.cells[ cell.$.cellIndex + ( backward ? -1 : 1 ) ];
// Invalid any empty value.
next.parentNode.parentNode;
return next;
}, function() {
var row = cell.getParent(),
table = row.getAscendant( 'table' ),
nextRow = table.$.rows[ row.$.rowIndex + ( backward ? -1 : 1 ) ];
return nextRow.cells[ backward ? nextRow.cells.length - 1 : 0 ];
} );
// Clone one more row at the end of table and select the first newly established cell.
if ( !( next || backward ) ) {
var table = cell.getAscendant( 'table' ).$,
cells = cell.getParent().$.cells;
var newRow = new CKEDITOR.dom.element( table.insertRow( -1 ), editor.document );
for ( var i = 0, count = cells.length; i < count; i++ ) {
var newCell = newRow.append( new CKEDITOR.dom.element( cells[ i ], editor.document ).clone( false, false ) );
newCell.appendBogus();
}
resultRange.moveToElementEditStart( newRow );
} else if ( next ) {
next = new CKEDITOR.dom.element( next );
resultRange.moveToElementEditStart( next );
// Avoid selecting empty block makes the cursor blind.
if ( !( resultRange.checkStartOfBlock() && resultRange.checkEndOfBlock() ) )
resultRange.selectNodeContents( next );
} else
return true;
resultRange.select( true );
return true;
}
}
return false;
}
};
}
CKEDITOR.plugins.add( 'tab', {
init: function( editor ) {
var tabTools = editor.config.enableTabKeyTools !== false,
tabSpaces = editor.config.tabSpaces || 0,
tabText = '';
while ( tabSpaces-- )
tabText += '\xa0';
if ( tabText ) {
editor.on( 'key', function( ev ) {
if ( ev.data.keyCode == 9 ) // TAB
{
editor.insertHtml( tabText );
ev.cancel();
}
} );
}
if ( tabTools ) {
editor.on( 'key', function( ev ) {
if ( ev.data.keyCode == 9 && editor.execCommand( 'selectNextCell' ) || // TAB
ev.data.keyCode == ( CKEDITOR.SHIFT + 9 ) && editor.execCommand( 'selectPreviousCell' ) ) // SHIFT+TAB
ev.cancel();
} );
}
editor.addCommand( 'blur', CKEDITOR.tools.extend( blurCommand, meta ) );
editor.addCommand( 'blurBack', CKEDITOR.tools.extend( blurBackCommand, meta ) );
editor.addCommand( 'selectNextCell', selectNextCellCommand() );
editor.addCommand( 'selectPreviousCell', selectNextCellCommand( true ) );
}
} );
} )();
/**
* Moves the UI focus to the element following this element in the tabindex order.
*
* var element = CKEDITOR.document.getById( 'example' );
* element.focusNext();
*
* @param {Boolean} [ignoreChildren=false]
* @param {Number} [indexToUse]
* @member CKEDITOR.dom.element
*/
CKEDITOR.dom.element.prototype.focusNext = function( ignoreChildren, indexToUse ) {
var $ = this.$,
curTabIndex = ( indexToUse === undefined ? this.getTabIndex() : indexToUse ),
passedCurrent, enteredCurrent, elected, electedTabIndex, element, elementTabIndex;
if ( curTabIndex <= 0 ) {
// If this element has tabindex <= 0 then we must simply look for any
// element following it containing tabindex=0.
element = this.getNextSourceNode( ignoreChildren, CKEDITOR.NODE_ELEMENT );
while ( element ) {
if ( element.isVisible() && element.getTabIndex() === 0 ) {
elected = element;
break;
}
element = element.getNextSourceNode( false, CKEDITOR.NODE_ELEMENT );
}
} else {
// If this element has tabindex > 0 then we must look for:
// 1. An element following this element with the same tabindex.
// 2. The first element in source other with the lowest tabindex
// that is higher than this element tabindex.
// 3. The first element with tabindex=0.
element = this.getDocument().getBody().getFirst();
while ( ( element = element.getNextSourceNode( false, CKEDITOR.NODE_ELEMENT ) ) ) {
if ( !passedCurrent ) {
if ( !enteredCurrent && element.equals( this ) ) {
enteredCurrent = true;
// Ignore this element, if required.
if ( ignoreChildren ) {
if ( !( element = element.getNextSourceNode( true, CKEDITOR.NODE_ELEMENT ) ) )
break;
passedCurrent = 1;
}
} else if ( enteredCurrent && !this.contains( element ) )
passedCurrent = 1;
}
if ( !element.isVisible() || ( elementTabIndex = element.getTabIndex() ) < 0 )
continue;
if ( passedCurrent && elementTabIndex == curTabIndex ) {
elected = element;
break;
}
if ( elementTabIndex > curTabIndex && ( !elected || !electedTabIndex || elementTabIndex < electedTabIndex ) ) {
elected = element;
electedTabIndex = elementTabIndex;
} else if ( !elected && elementTabIndex === 0 ) {
elected = element;
electedTabIndex = elementTabIndex;
}
}
}
if ( elected )
elected.focus();
};
/**
* Moves the UI focus to the element before this element in the tabindex order.
*
* var element = CKEDITOR.document.getById( 'example' );
* element.focusPrevious();
*
* @param {Boolean} [ignoreChildren=false]
* @param {Number} [indexToUse]
* @member CKEDITOR.dom.element
*/
CKEDITOR.dom.element.prototype.focusPrevious = function( ignoreChildren, indexToUse ) {
var $ = this.$,
curTabIndex = ( indexToUse === undefined ? this.getTabIndex() : indexToUse ),
passedCurrent, enteredCurrent, elected,
electedTabIndex = 0,
elementTabIndex;
var element = this.getDocument().getBody().getLast();
while ( ( element = element.getPreviousSourceNode( false, CKEDITOR.NODE_ELEMENT ) ) ) {
if ( !passedCurrent ) {
if ( !enteredCurrent && element.equals( this ) ) {
enteredCurrent = true;
// Ignore this element, if required.
if ( ignoreChildren ) {
if ( !( element = element.getPreviousSourceNode( true, CKEDITOR.NODE_ELEMENT ) ) )
break;
passedCurrent = 1;
}
} else if ( enteredCurrent && !this.contains( element ) )
passedCurrent = 1;
}
if ( !element.isVisible() || ( elementTabIndex = element.getTabIndex() ) < 0 )
continue;
if ( curTabIndex <= 0 ) {
// If this element has tabindex <= 0 then we must look for:
// 1. An element before this one containing tabindex=0.
// 2. The last element with the highest tabindex.
if ( passedCurrent && elementTabIndex === 0 ) {
elected = element;
break;
}
if ( elementTabIndex > electedTabIndex ) {
elected = element;
electedTabIndex = elementTabIndex;
}
} else {
// If this element has tabindex > 0 we must look for:
// 1. An element preceeding this one, with the same tabindex.
// 2. The last element in source other with the highest tabindex
// that is lower than this element tabindex.
if ( passedCurrent && elementTabIndex == curTabIndex ) {
elected = element;
break;
}
if ( elementTabIndex < curTabIndex && ( !elected || elementTabIndex > electedTabIndex ) ) {
elected = element;
electedTabIndex = elementTabIndex;
}
}
}
if ( elected )
elected.focus();
};
/**
* Intructs the editor to add a number of spaces (`&nbsp;`) to the text when
* hitting the *TAB* key. If set to zero, the *TAB* key will be used to move the
* cursor focus to the next element in the page, out of the editor focus.
*
* config.tabSpaces = 4;
*
* @cfg {Number} [tabSpaces=0]
* @member CKEDITOR.config
*/
/**
* Allow context-sensitive tab key behaviors, including the following scenarios:
*
* When selection is anchored inside **table cells**:
*
* * If *TAB* is pressed, select the contents of the "next" cell. If in the last
* cell in the table, add a new row to it and focus its first cell.
* * If *SHIFT+TAB* is pressed, select the contents of the "previous" cell.
* Do nothing when it's in the first cell.
*
* Example:
*
* config.enableTabKeyTools = false;
*
* @cfg {Boolean} [enableTabKeyTools=true]
* @member CKEDITOR.config
*/
// If the TAB key is not supposed to be enabled for navigation, the following
// settings could be used alternatively:
// config.keystrokes.push(
// [ CKEDITOR.ALT + 38 /*Arrow Up*/, 'selectPreviousCell' ],
// [ CKEDITOR.ALT + 40 /*Arrow Down*/, 'selectNextCell' ]
// );

View File

@ -0,0 +1,106 @@
/**
* @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
CKEDITOR.plugins.add( 'table', {
requires: 'dialog',
lang: 'af,ar,bg,bn,bs,ca,cs,cy,da,de,el,en,en-au,en-ca,en-gb,eo,es,et,eu,fa,fi,fo,fr,fr-ca,gl,gu,he,hi,hr,hu,id,is,it,ja,ka,km,ko,ku,lt,lv,mk,mn,ms,nb,nl,no,pl,pt,pt-br,ro,ru,si,sk,sl,sq,sr,sr-latn,sv,th,tr,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE%
icons: 'table', // %REMOVE_LINE_CORE%
hidpi: true, // %REMOVE_LINE_CORE%
init: function( editor ) {
if ( editor.blockless )
return;
var table = CKEDITOR.plugins.table,
lang = editor.lang.table;
editor.addCommand( 'table', new CKEDITOR.dialogCommand( 'table', {
context: 'table',
allowedContent: 'table{width,height}[align,border,cellpadding,cellspacing,summary];' +
'caption tbody thead tfoot;' +
'th td tr[scope];' +
( editor.plugins.dialogadvtab ? 'table' + editor.plugins.dialogadvtab.allowedContent() : '' ),
requiredContent: 'table',
contentTransformations: [
[ 'table{width}: sizeToStyle', 'table[width]: sizeToAttribute' ]
]
} ) );
function createDef( def ) {
return CKEDITOR.tools.extend( def || {}, {
contextSensitive: 1,
refresh: function( editor, path ) {
this.setState( path.contains( 'table', 1 ) ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED );
}
} );
}
editor.addCommand( 'tableProperties', new CKEDITOR.dialogCommand( 'tableProperties', createDef() ) );
editor.addCommand( 'tableDelete', createDef( {
exec: function( editor ) {
var path = editor.elementPath(),
table = path.contains( 'table', 1 );
if ( !table )
return;
// If the table's parent has only one child remove it as well (unless it's the body or a table cell) (#5416, #6289)
var parent = table.getParent();
if ( parent.getChildCount() == 1 && !parent.is( 'body', 'td', 'th' ) )
table = parent;
var range = editor.createRange();
range.moveToPosition( table, CKEDITOR.POSITION_BEFORE_START );
table.remove();
range.select();
}
} ) );
editor.ui.addButton && editor.ui.addButton( 'Table', {
label: lang.toolbar,
command: 'table',
toolbar: 'insert,30'
} );
CKEDITOR.dialog.add( 'table', this.path + 'dialogs/table.js' );
CKEDITOR.dialog.add( 'tableProperties', this.path + 'dialogs/table.js' );
// If the "menu" plugin is loaded, register the menu items.
if ( editor.addMenuItems ) {
editor.addMenuItems( {
table: {
label: lang.menu,
command: 'tableProperties',
group: 'table',
order: 5
},
tabledelete: {
label: lang.deleteTable,
command: 'tableDelete',
group: 'table',
order: 1
}
} );
}
editor.on( 'doubleclick', function( evt ) {
var element = evt.data.element;
if ( element.is( 'table' ) )
evt.data.dialog = 'tableProperties';
} );
// If the "contextmenu" plugin is loaded, register the listeners.
if ( editor.contextMenu ) {
editor.contextMenu.addListener( function() {
// menu item state is resolved on commands.
return {
tabledelete: CKEDITOR.TRISTATE_OFF,
table: CKEDITOR.TRISTATE_OFF
};
} );
}
}
} );

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,781 @@
/**
* @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
/**
* @fileOverview The "toolbar" plugin. Renders the default toolbar interface in
* the editor.
*/
( function() {
var toolbox = function() {
this.toolbars = [];
this.focusCommandExecuted = false;
};
toolbox.prototype.focus = function() {
for ( var t = 0, toolbar; toolbar = this.toolbars[ t++ ]; ) {
for ( var i = 0, item; item = toolbar.items[ i++ ]; ) {
if ( item.focus ) {
item.focus();
return;
}
}
}
};
var commands = {
toolbarFocus: {
modes: { wysiwyg: 1, source: 1 },
readOnly: 1,
exec: function( editor ) {
if ( editor.toolbox ) {
editor.toolbox.focusCommandExecuted = true;
// Make the first button focus accessible for IE. (#3417)
// Adobe AIR instead need while of delay.
if ( CKEDITOR.env.ie || CKEDITOR.env.air )
setTimeout( function() {
editor.toolbox.focus();
}, 100 );
else
editor.toolbox.focus();
}
}
}
};
CKEDITOR.plugins.add( 'toolbar', {
requires: 'button',
lang: 'af,ar,bg,bn,bs,ca,cs,cy,da,de,el,en,en-au,en-ca,en-gb,eo,es,et,eu,fa,fi,fo,fr,fr-ca,gl,gu,he,hi,hr,hu,id,is,it,ja,ka,km,ko,ku,lt,lv,mk,mn,ms,nb,nl,no,pl,pt,pt-br,ro,ru,si,sk,sl,sq,sr,sr-latn,sv,th,tr,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE%
init: function( editor ) {
var endFlag;
var itemKeystroke = function( item, keystroke ) {
var next, toolbar;
var rtl = editor.lang.dir == 'rtl',
toolbarGroupCycling = editor.config.toolbarGroupCycling,
// Picking right/left key codes.
rightKeyCode = rtl ? 37 : 39,
leftKeyCode = rtl ? 39 : 37;
toolbarGroupCycling = toolbarGroupCycling === undefined || toolbarGroupCycling;
switch ( keystroke ) {
case 9: // TAB
case CKEDITOR.SHIFT + 9: // SHIFT + TAB
// Cycle through the toolbars, starting from the one
// closest to the current item.
while ( !toolbar || !toolbar.items.length ) {
toolbar = keystroke == 9 ? ( ( toolbar ? toolbar.next : item.toolbar.next ) || editor.toolbox.toolbars[ 0 ] ) : ( ( toolbar ? toolbar.previous : item.toolbar.previous ) || editor.toolbox.toolbars[ editor.toolbox.toolbars.length - 1 ] );
// Look for the first item that accepts focus.
if ( toolbar.items.length ) {
item = toolbar.items[ endFlag ? ( toolbar.items.length - 1 ) : 0 ];
while ( item && !item.focus ) {
item = endFlag ? item.previous : item.next;
if ( !item )
toolbar = 0;
}
}
}
if ( item )
item.focus();
return false;
case rightKeyCode:
next = item;
do {
// Look for the next item in the toolbar.
next = next.next;
// If it's the last item, cycle to the first one.
if ( !next && toolbarGroupCycling ) next = item.toolbar.items[ 0 ];
}
while ( next && !next.focus );
// If available, just focus it, otherwise focus the
// first one.
if ( next )
next.focus();
else
// Send a TAB.
itemKeystroke( item, 9 );
return false;
case 40: // DOWN-ARROW
if ( item.button && item.button.hasArrow ) {
// Note: code is duplicated in plugins\richcombo\plugin.js in keyDownFn().
editor.once( 'panelShow', function( evt ) {
evt.data._.panel._.currentBlock.onKeyDown( 40 );
} );
item.execute();
} else {
// Send left arrow key.
itemKeystroke( item, keystroke == 40 ? rightKeyCode : leftKeyCode );
}
return false;
case leftKeyCode:
case 38: // UP-ARROW
next = item;
do {
// Look for the previous item in the toolbar.
next = next.previous;
// If it's the first item, cycle to the last one.
if ( !next && toolbarGroupCycling ) next = item.toolbar.items[ item.toolbar.items.length - 1 ];
}
while ( next && !next.focus );
// If available, just focus it, otherwise focus the
// last one.
if ( next )
next.focus();
else {
endFlag = 1;
// Send a SHIFT + TAB.
itemKeystroke( item, CKEDITOR.SHIFT + 9 );
endFlag = 0;
}
return false;
case 27: // ESC
editor.focus();
return false;
case 13: // ENTER
case 32: // SPACE
item.execute();
return false;
}
return true;
};
editor.on( 'uiSpace', function( event ) {
if ( event.data.space != editor.config.toolbarLocation )
return;
// Create toolbar only once.
event.removeListener();
editor.toolbox = new toolbox();
var labelId = CKEDITOR.tools.getNextId();
var output = [
'<span id="', labelId, '" class="cke_voice_label">', editor.lang.toolbar.toolbars, '</span>',
'<span id="' + editor.ui.spaceId( 'toolbox' ) + '" class="cke_toolbox" role="group" aria-labelledby="', labelId, '" onmousedown="return false;">' ];
var expanded = editor.config.toolbarStartupExpanded !== false,
groupStarted, pendingSeparator;
// If the toolbar collapser will be available, we'll have
// an additional container for all toolbars.
if ( editor.config.toolbarCanCollapse && editor.elementMode != CKEDITOR.ELEMENT_MODE_INLINE )
output.push( '<span class="cke_toolbox_main"' + ( expanded ? '>' : ' style="display:none">' ) );
var toolbars = editor.toolbox.toolbars,
toolbar = getToolbarConfig( editor );
for ( var r = 0; r < toolbar.length; r++ ) {
var toolbarId,
toolbarObj = 0,
toolbarName,
row = toolbar[ r ],
items;
// It's better to check if the row object is really
// available because it's a common mistake to leave
// an extra comma in the toolbar definition
// settings, which leads on the editor not loading
// at all in IE. (#3983)
if ( !row )
continue;
if ( groupStarted ) {
output.push( '</span>' );
groupStarted = 0;
pendingSeparator = 0;
}
if ( row === '/' ) {
output.push( '<span class="cke_toolbar_break"></span>' );
continue;
}
items = row.items || row;
// Create all items defined for this toolbar.
for ( var i = 0; i < items.length; i++ ) {
var item = items[ i ],
canGroup;
if ( item ) {
if ( item.type == CKEDITOR.UI_SEPARATOR ) {
// Do not add the separator immediately. Just save
// it be included if we already have something in
// the toolbar and if a new item is to be added (later).
pendingSeparator = groupStarted && item;
continue;
}
canGroup = item.canGroup !== false;
// Initialize the toolbar first, if needed.
if ( !toolbarObj ) {
// Create the basic toolbar object.
toolbarId = CKEDITOR.tools.getNextId();
toolbarObj = { id: toolbarId, items: [] };
toolbarName = row.name && ( editor.lang.toolbar.toolbarGroups[ row.name ] || row.name );
// Output the toolbar opener.
output.push( '<span id="', toolbarId, '" class="cke_toolbar"', ( toolbarName ? ' aria-labelledby="' + toolbarId + '_label"' : '' ), ' role="toolbar">' );
// If a toolbar name is available, send the voice label.
toolbarName && output.push( '<span id="', toolbarId, '_label" class="cke_voice_label">', toolbarName, '</span>' );
output.push( '<span class="cke_toolbar_start"></span>' );
// Add the toolbar to the "editor.toolbox.toolbars"
// array.
var index = toolbars.push( toolbarObj ) - 1;
// Create the next/previous reference.
if ( index > 0 ) {
toolbarObj.previous = toolbars[ index - 1 ];
toolbarObj.previous.next = toolbarObj;
}
}
if ( canGroup ) {
if ( !groupStarted ) {
output.push( '<span class="cke_toolgroup" role="presentation">' );
groupStarted = 1;
}
} else if ( groupStarted ) {
output.push( '</span>' );
groupStarted = 0;
}
function addItem( item ) {
var itemObj = item.render( editor, output );
index = toolbarObj.items.push( itemObj ) - 1;
if ( index > 0 ) {
itemObj.previous = toolbarObj.items[ index - 1 ];
itemObj.previous.next = itemObj;
}
itemObj.toolbar = toolbarObj;
itemObj.onkey = itemKeystroke;
// Fix for #3052:
// Prevent JAWS from focusing the toolbar after document load.
itemObj.onfocus = function() {
if ( !editor.toolbox.focusCommandExecuted )
editor.focus();
};
}
if ( pendingSeparator ) {
addItem( pendingSeparator );
pendingSeparator = 0;
}
addItem( item );
}
}
if ( groupStarted ) {
output.push( '</span>' );
groupStarted = 0;
pendingSeparator = 0;
}
if ( toolbarObj )
output.push( '<span class="cke_toolbar_end"></span></span>' );
}
if ( editor.config.toolbarCanCollapse )
output.push( '</span>' );
// Not toolbar collapser for inline mode.
if ( editor.config.toolbarCanCollapse && editor.elementMode != CKEDITOR.ELEMENT_MODE_INLINE ) {
var collapserFn = CKEDITOR.tools.addFunction( function() {
editor.execCommand( 'toolbarCollapse' );
} );
editor.on( 'destroy', function() {
CKEDITOR.tools.removeFunction( collapserFn );
} );
editor.addCommand( 'toolbarCollapse', {
readOnly: 1,
exec: function( editor ) {
var collapser = editor.ui.space( 'toolbar_collapser' ),
toolbox = collapser.getPrevious(),
contents = editor.ui.space( 'contents' ),
toolboxContainer = toolbox.getParent(),
contentHeight = parseInt( contents.$.style.height, 10 ),
previousHeight = toolboxContainer.$.offsetHeight,
minClass = 'cke_toolbox_collapser_min',
collapsed = collapser.hasClass( minClass );
if ( !collapsed ) {
toolbox.hide();
collapser.addClass( minClass );
collapser.setAttribute( 'title', editor.lang.toolbar.toolbarExpand );
} else {
toolbox.show();
collapser.removeClass( minClass );
collapser.setAttribute( 'title', editor.lang.toolbar.toolbarCollapse );
}
// Update collapser symbol.
collapser.getFirst().setText( collapsed ? '\u25B2' : // BLACK UP-POINTING TRIANGLE
'\u25C0' ); // BLACK LEFT-POINTING TRIANGLE
var dy = toolboxContainer.$.offsetHeight - previousHeight;
contents.setStyle( 'height', ( contentHeight - dy ) + 'px' );
editor.fire( 'resize' );
},
modes: { wysiwyg: 1, source: 1 }
} );
editor.setKeystroke( CKEDITOR.ALT + ( CKEDITOR.env.ie || CKEDITOR.env.webkit ? 189 : 109 ) /*-*/, 'toolbarCollapse' );
output.push( '<a title="' + ( expanded ? editor.lang.toolbar.toolbarCollapse : editor.lang.toolbar.toolbarExpand )
+ '" id="' + editor.ui.spaceId( 'toolbar_collapser' )
+ '" tabIndex="-1" class="cke_toolbox_collapser' );
if ( !expanded )
output.push( ' cke_toolbox_collapser_min' );
output.push( '" onclick="CKEDITOR.tools.callFunction(' + collapserFn + ')">', '<span class="cke_arrow">&#9650;</span>', // BLACK UP-POINTING TRIANGLE
'</a>' );
}
output.push( '</span>' );
event.data.html += output.join( '' );
} );
editor.on( 'destroy', function() {
if ( this.toolbox )
{
var toolbars,
index = 0,
i, items, instance;
toolbars = this.toolbox.toolbars;
for ( ; index < toolbars.length; index++ ) {
items = toolbars[ index ].items;
for ( i = 0; i < items.length; i++ ) {
instance = items[ i ];
if ( instance.clickFn )
CKEDITOR.tools.removeFunction( instance.clickFn );
if ( instance.keyDownFn )
CKEDITOR.tools.removeFunction( instance.keyDownFn );
}
}
}
} );
// Manage editor focus when navigating the toolbar.
editor.on( 'uiReady', function() {
var toolbox = editor.ui.space( 'toolbox' );
toolbox && editor.focusManager.add( toolbox, 1 );
} );
editor.addCommand( 'toolbarFocus', commands.toolbarFocus );
editor.setKeystroke( CKEDITOR.ALT + 121 /*F10*/, 'toolbarFocus' );
editor.ui.add( '-', CKEDITOR.UI_SEPARATOR, {} );
editor.ui.addHandler( CKEDITOR.UI_SEPARATOR, {
create: function() {
return {
render: function( editor, output ) {
output.push( '<span class="cke_toolbar_separator" role="separator"></span>' );
return {};
}
};
}
} );
}
} );
function getToolbarConfig( editor ) {
var removeButtons = editor.config.removeButtons;
removeButtons = removeButtons && removeButtons.split( ',' );
function buildToolbarConfig() {
// Object containing all toolbar groups used by ui items.
var lookup = getItemDefinedGroups();
// Take the base for the new toolbar, which is basically a toolbar
// definition without items.
var toolbar = CKEDITOR.tools.clone( editor.config.toolbarGroups ) || getPrivateToolbarGroups( editor );
// Fill the toolbar groups with the available ui items.
for ( var i = 0; i < toolbar.length; i++ ) {
var toolbarGroup = toolbar[ i ];
// Skip toolbar break.
if ( toolbarGroup == '/' )
continue;
// Handle simply group name item.
else if ( typeof toolbarGroup == 'string' )
toolbarGroup = toolbar[ i ] = { name: toolbarGroup };
var items, subGroups = toolbarGroup.groups;
// Look for items that match sub groups.
if ( subGroups ) {
for ( var j = 0, sub; j < subGroups.length; j++ ) {
sub = subGroups[ j ];
// If any ui item is registered for this subgroup.
items = lookup[ sub ];
items && fillGroup( toolbarGroup, items );
}
}
// Add the main group items as well.
items = lookup[ toolbarGroup.name ];
items && fillGroup( toolbarGroup, items );
}
return toolbar;
}
// Returns an object containing all toolbar groups used by ui items.
function getItemDefinedGroups() {
var groups = {},
itemName, item, itemToolbar, group, order;
for ( itemName in editor.ui.items ) {
item = editor.ui.items[ itemName ];
itemToolbar = item.toolbar || 'others';
if ( itemToolbar ) {
// Break the toolbar property into its parts: "group_name[,order]".
itemToolbar = itemToolbar.split( ',' );
group = itemToolbar[ 0 ];
order = parseInt( itemToolbar[ 1 ] || -1, 10 );
// Initialize the group, if necessary.
groups[ group ] || ( groups[ group ] = [] );
// Push the data used to build the toolbar later.
groups[ group ].push( { name: itemName, order: order } );
}
}
// Put the items in the right order.
for ( group in groups ) {
groups[ group ] = groups[ group ].sort( function( a, b ) {
return a.order == b.order ? 0 :
b.order < 0 ? -1 :
a.order < 0 ? 1 :
a.order < b.order ? -1 :
1;
} );
}
return groups;
}
function fillGroup( toolbarGroup, uiItems ) {
if ( uiItems.length ) {
if ( toolbarGroup.items )
toolbarGroup.items.push( editor.ui.create( '-' ) );
else
toolbarGroup.items = [];
var item, name;
while ( ( item = uiItems.shift() ) ) {
name = typeof item == 'string' ? item : item.name;
// Ignore items that are configured to be removed.
if ( !removeButtons || CKEDITOR.tools.indexOf( removeButtons, name ) == -1 ) {
item = editor.ui.create( name );
if ( !item )
continue;
if ( !editor.addFeature( item ) )
continue;
toolbarGroup.items.push( item );
}
}
}
}
function populateToolbarConfig( config ) {
var toolbar = [],
i, group, newGroup;
for ( i = 0; i < config.length; ++i ) {
group = config[ i ];
newGroup = {};
if ( group == '/' )
toolbar.push( group );
else if ( CKEDITOR.tools.isArray( group ) ) {
fillGroup( newGroup, CKEDITOR.tools.clone( group ) );
toolbar.push( newGroup );
}
else if ( group.items ) {
fillGroup( newGroup, CKEDITOR.tools.clone( group.items ) );
newGroup.name = group.name;
toolbar.push( newGroup );
}
}
return toolbar;
}
var toolbar = editor.config.toolbar;
// If it is a string, return the relative "toolbar_name" config.
if ( typeof toolbar == 'string' )
toolbar = editor.config[ 'toolbar_' + toolbar ];
return ( editor.toolbar = toolbar ? populateToolbarConfig( toolbar ) : buildToolbarConfig() );
}
/**
* Add toolbar group. See {@link CKEDITOR.config#toolbarGroups} for more details.
*
* **Note:** This method won't modify toolbar groups set explicitly by
* {@link CKEDITOR.config#toolbarGroups}. It will extend only default setting.
*
* @param {String} name Group name.
* @param {Number/String} previous Name of group after which this one
* should be added or `0` if this group should be the first one.
* @param {String} [subgroupOf] Name of parent group.
* @member CKEDITOR.ui
*/
CKEDITOR.ui.prototype.addToolbarGroup = function( name, previous, subgroupOf ) {
// The toolbarGroups from the privates is the one we gonna use for automatic toolbar creation.
var toolbarGroups = getPrivateToolbarGroups( this.editor ),
atStart = previous === 0,
newGroup = { name: name };
if ( subgroupOf ) {
// Transform the subgroupOf name in the real subgroup object.
subgroupOf = CKEDITOR.tools.search( toolbarGroups, function( group ) {
return group.name == subgroupOf;
} );
if ( subgroupOf ) {
!subgroupOf.groups && ( subgroupOf.groups = [] ) ;
if ( previous ) {
// Search the "previous" item and add the new one after it.
previous = CKEDITOR.tools.indexOf( subgroupOf.groups, previous );
if ( previous >= 0 ) {
subgroupOf.groups.splice( previous + 1, 0, name );
return;
}
}
// If no previous found.
if ( atStart )
subgroupOf.groups.splice( 0, 0, name );
else
subgroupOf.groups.push( name );
return;
} else {
// Ignore "previous" if subgroupOf has not been found.
previous = null;
}
}
if ( previous ) {
// Transform the "previous" name into its index.
previous = CKEDITOR.tools.indexOf( toolbarGroups, function( group ) {
return group.name == previous;
} );
}
if ( atStart )
toolbarGroups.splice( 0, 0, name );
else if ( typeof previous == 'number' )
toolbarGroups.splice( previous + 1, 0, newGroup );
else
toolbarGroups.push( name );
};
function getPrivateToolbarGroups( editor ) {
return editor._.toolbarGroups || ( editor._.toolbarGroups = [
{ name: 'document', groups: [ 'mode', 'document', 'doctools' ] },
{ name: 'clipboard', groups: [ 'clipboard', 'undo' ] },
{ name: 'editing', groups: [ 'find', 'selection', 'spellchecker' ] },
{ name: 'forms' },
'/',
{ name: 'basicstyles', groups: [ 'basicstyles', 'cleanup' ] },
{ name: 'paragraph', groups: [ 'list', 'indent', 'blocks', 'align', 'bidi' ] },
{ name: 'links' },
{ name: 'insert' },
'/',
{ name: 'styles' },
{ name: 'colors' },
{ name: 'tools' },
{ name: 'others' },
{ name: 'about' }
] );
}
} )();
/**
* Separator UI element.
*
* @readonly
* @property {String} [='separator']
* @member CKEDITOR
*/
CKEDITOR.UI_SEPARATOR = 'separator';
/**
* The "UI space" to which rendering the toolbar. For the default editor implementation,
* the recommended options are `'top'` and `'bottom'`.
*
* config.toolbarLocation = 'bottom';
*
* @cfg
* @member CKEDITOR.config
*/
CKEDITOR.config.toolbarLocation = 'top';
/**
* The toolbox (alias toolbar) definition. It is a toolbar name or an array of
* toolbars (strips), each one being also an array, containing a list of UI items.
*
* If set to `null`, generate toolbar automatically using all available buttons
* and {@link #toolbarGroups} as a toolbar groups layout.
*
* // Defines a toolbar with only one strip containing the "Source" button, a
* // separator and the "Bold" and "Italic" buttons.
* config.toolbar = [
* [ 'Source', '-', 'Bold', 'Italic' ]
* ];
*
* // Similar to example the above, defines a "Basic" toolbar with only one strip containing three buttons.
* // Note that this setting is composed by "toolbar_" added by the toolbar name, which in this case is called "Basic".
* // This second part of the setting name can be anything. You must use this name in the CKEDITOR.config.toolbar setting,
* // so you instruct the editor which toolbar_(name) setting to use.
* config.toolbar_Basic = [
* [ 'Source', '-', 'Bold', 'Italic' ]
* ];
* // Load toolbar_Name where Name = Basic.
* config.toolbar = 'Basic';
*
* @cfg {Array/String} [toolbar=null]
* @member CKEDITOR.config
*/
/**
* The toolbar groups definition.
*
* If toolbar layout isn't explicitly defined by {@link #toolbar} setting, then
* this setting is used to group all defined buttons (see {@link CKEDITOR.ui#addButton}).
* Buttons are associated with toolbar groups by `toolbar` property in their definition objects.
*
* New groups may be dynamically added during the editor and plugins initialization by
* {@link CKEDITOR.ui#addToolbarGroup}. Although only if default setting was used.
*
* // Default setting.
* config.toolbarGroups = [
* { name: 'document', groups: [ 'mode', 'document', 'doctools' ] },
* { name: 'clipboard', groups: [ 'clipboard', 'undo' ] },
* { name: 'editing', groups: [ 'find', 'selection', 'spellchecker' ] },
* { name: 'forms' },
* '/',
* { name: 'basicstyles', groups: [ 'basicstyles', 'cleanup' ] },
* { name: 'paragraph', groups: [ 'list', 'indent', 'blocks', 'align', 'bidi' ] },
* { name: 'links' },
* { name: 'insert' },
* '/',
* { name: 'styles' },
* { name: 'colors' },
* { name: 'tools' },
* { name: 'others' },
* { name: 'about' }
* ];
*
* @cfg {Array} [toolbarGroups=see example]
* @member CKEDITOR.config
*/
/**
* Whether the toolbar can be collapsed by the user. If disabled, the collapser
* button will not be displayed.
*
* config.toolbarCanCollapse = true;
*
* @cfg {Boolean} [toolbarCanCollapse=false]
* @member CKEDITOR.config
*/
/**
* Whether the toolbar must start expanded when the editor is loaded.
*
* Setting this option to `false` will affect toolbar only when
* {@link #toolbarCanCollapse} is set to `true`:
*
* config.toolbarCanCollapse = true;
* config.toolbarStartupExpanded = false;
*
* @cfg {Boolean} [toolbarStartupExpanded=true]
* @member CKEDITOR.config
*/
/**
* When enabled, makes the arrow keys navigation cycle within the current
* toolbar group. Otherwise the arrows will move through all items available in
* the toolbar. The *TAB* key will still be used to quickly jump among the
* toolbar groups.
*
* config.toolbarGroupCycling = false;
*
* @since 3.6
* @cfg {Boolean} [toolbarGroupCycling=true]
* @member CKEDITOR.config
*/
/**
* List of toolbar button names that must not be rendered. This will work as
* well for non-button toolbar items, like the Font combos.
*
* config.removeButtons = 'Underline,JustifyCenter';
*
* This configuration should not be overused, having
* {@link CKEDITOR.config#removePlugins} removing features from the editor. In
* some cases though, a single plugin may define a set of toolbar buttons and
* removeButtons may be useful when just a few of them are to be removed.
*
* @cfg {String} [removeButtons]
* @member CKEDITOR.config
*/
/**
* Toolbar definition used by the editor. It is crated from the
* {@link CKEDITOR.config#toolbar} if it is set or automatically
* based on {@link CKEDITOR.config#toolbarGroups}.
*
* @readonly
* @property {Object} toolbar
* @member CKEDITOR.editor
*/

View File

@ -0,0 +1,738 @@
/**
* @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
/**
* @fileOverview Undo/Redo system for saving a shapshot for document modification
* and other recordable changes.
*/
( function() {
CKEDITOR.plugins.add( 'undo', {
lang: 'af,ar,bg,bn,bs,ca,cs,cy,da,de,el,en,en-au,en-ca,en-gb,eo,es,et,eu,fa,fi,fo,fr,fr-ca,gl,gu,he,hi,hr,hu,id,is,it,ja,ka,km,ko,ku,lt,lv,mk,mn,ms,nb,nl,no,pl,pt,pt-br,ro,ru,si,sk,sl,sq,sr,sr-latn,sv,th,tr,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE%
icons: 'redo,redo-rtl,undo,undo-rtl', // %REMOVE_LINE_CORE%
hidpi: true, // %REMOVE_LINE_CORE%
init: function( editor ) {
var undoManager = editor.undoManager = new UndoManager( editor );
var undoCommand = editor.addCommand( 'undo', {
exec: function() {
if ( undoManager.undo() ) {
editor.selectionChange();
this.fire( 'afterUndo' );
}
},
startDisabled: true,
canUndo: false
} );
var redoCommand = editor.addCommand( 'redo', {
exec: function() {
if ( undoManager.redo() ) {
editor.selectionChange();
this.fire( 'afterRedo' );
}
},
startDisabled: true,
canUndo: false
} );
editor.setKeystroke( [
[ CKEDITOR.CTRL + 90 /*Z*/, 'undo' ],
[ CKEDITOR.CTRL + 89 /*Y*/, 'redo' ],
[ CKEDITOR.CTRL + CKEDITOR.SHIFT + 90 /*Z*/, 'redo' ]
] );
undoManager.onChange = function() {
undoCommand.setState( undoManager.undoable() ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED );
redoCommand.setState( undoManager.redoable() ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED );
};
function recordCommand( event ) {
// If the command hasn't been marked to not support undo.
if ( undoManager.enabled && event.data.command.canUndo !== false )
undoManager.save();
}
// We'll save snapshots before and after executing a command.
editor.on( 'beforeCommandExec', recordCommand );
editor.on( 'afterCommandExec', recordCommand );
// Save snapshots before doing custom changes.
editor.on( 'saveSnapshot', function( evt ) {
undoManager.save( evt.data && evt.data.contentOnly );
} );
// Registering keydown on every document recreation.(#3844)
editor.on( 'contentDom', function() {
editor.editable().on( 'keydown', function( event ) {
var keystroke = event.data.getKey();
if ( keystroke == 8 /*Backspace*/ || keystroke == 46 /*Delete*/ )
undoManager.type( keystroke, 0 );
} );
editor.editable().on( 'keypress', function( event ) {
undoManager.type( event.data.getKey(), 1 );
} );
} );
// Always save an undo snapshot - the previous mode might have
// changed editor contents.
editor.on( 'beforeModeUnload', function() {
editor.mode == 'wysiwyg' && undoManager.save( true );
} );
function toggleUndoManager() {
undoManager.enabled = editor.readOnly ? false : editor.mode == 'wysiwyg';
undoManager.onChange();
}
// Make the undo manager available only in wysiwyg mode.
editor.on( 'mode', toggleUndoManager );
// Disable undo manager when in read-only mode.
editor.on( 'readOnly', toggleUndoManager );
if ( editor.ui.addButton ) {
editor.ui.addButton( 'Undo', {
label: editor.lang.undo.undo,
command: 'undo',
toolbar: 'undo,10'
} );
editor.ui.addButton( 'Redo', {
label: editor.lang.undo.redo,
command: 'redo',
toolbar: 'undo,20'
} );
}
/**
* Resets the undo stack.
*
* @member CKEDITOR.editor
*/
editor.resetUndo = function() {
// Reset the undo stack.
undoManager.reset();
// Create the first image.
editor.fire( 'saveSnapshot' );
};
/**
* Amends the top of the undo stack (last undo image) with the current DOM changes.
*
* function() {
* editor.fire( 'saveSnapshot' );
* editor.document.body.append(...);
* // Makes new changes following the last undo snapshot a part of it.
* editor.fire( 'updateSnapshot' );
* ..
* }
*
* @event updateSnapshot
* @member CKEDITOR.editor
* @param {CKEDITOR.editor} editor This editor instance.
*/
editor.on( 'updateSnapshot', function() {
if ( undoManager.currentImage )
undoManager.update();
} );
/**
* Locks the undo manager to prevent any save/update operations.
*
* It is convenient to lock the undo manager before performing DOM operations
* that should not be recored (e.g. auto paragraphing).
*
* See {@link CKEDITOR.plugins.undo.UndoManager#lock} for more details.
*
* **Note:** In order to unlock the undo manager, {@link #unlockSnapshot} has to be fired
* the same number of times that `lockSnapshot` has been fired.
*
* @since 4.0
* @event lockSnapshot
* @member CKEDITOR.editor
* @param {CKEDITOR.editor} editor This editor instance.
* @param data
* @param {Boolean} [data.dontUpdate] When set to `true` the last snapshot will not be updated
* with the current contents and selection. Read more in the {@link CKEDITOR.plugins.undo.UndoManager#lock} method.
*/
editor.on( 'lockSnapshot', function( evt ) {
undoManager.lock( evt.data && evt.data.dontUpdate );
} );
/**
* Unlocks the undo manager and updates the latest snapshot.
*
* @since 4.0
* @event unlockSnapshot
* @member CKEDITOR.editor
* @param {CKEDITOR.editor} editor This editor instance.
*/
editor.on( 'unlockSnapshot', undoManager.unlock, undoManager );
}
} );
CKEDITOR.plugins.undo = {};
/**
* Undoes the snapshot which represents the current document status.
*
* @private
* @class CKEDITOR.plugins.undo.Image
* @constructor Creates an Image class instance.
* @param {CKEDITOR.editor} editor The editor instance on which the image is created.
* @param {Boolean} [contentsOnly] If set to `true` image will contain only contents, without selection.
*/
var Image = CKEDITOR.plugins.undo.Image = function( editor, contentsOnly ) {
this.editor = editor;
editor.fire( 'beforeUndoImage' );
var contents = editor.getSnapshot();
// In IE, we need to remove the expando attributes.
if ( CKEDITOR.env.ie && contents )
contents = contents.replace( /\s+data-cke-expando=".*?"/g, '' );
this.contents = contents;
if ( !contentsOnly ) {
var selection = contents && editor.getSelection();
this.bookmarks = selection && selection.createBookmarks2( true );
}
editor.fire( 'afterUndoImage' );
};
// Attributes that browser may changing them when setting via innerHTML.
var protectedAttrs = /\b(?:href|src|name)="[^"]*?"/gi;
Image.prototype = {
equalsContent: function( otherImage ) {
var thisContents = this.contents,
otherContents = otherImage.contents;
// For IE6/7 : Comparing only the protected attribute values but not the original ones.(#4522)
if ( CKEDITOR.env.ie && ( CKEDITOR.env.ie7Compat || CKEDITOR.env.ie6Compat ) ) {
thisContents = thisContents.replace( protectedAttrs, '' );
otherContents = otherContents.replace( protectedAttrs, '' );
}
if ( thisContents != otherContents )
return false;
return true;
},
equalsSelection: function( otherImage ) {
var bookmarksA = this.bookmarks,
bookmarksB = otherImage.bookmarks;
if ( bookmarksA || bookmarksB ) {
if ( !bookmarksA || !bookmarksB || bookmarksA.length != bookmarksB.length )
return false;
for ( var i = 0; i < bookmarksA.length; i++ ) {
var bookmarkA = bookmarksA[ i ],
bookmarkB = bookmarksB[ i ];
if ( bookmarkA.startOffset != bookmarkB.startOffset || bookmarkA.endOffset != bookmarkB.endOffset || !CKEDITOR.tools.arrayCompare( bookmarkA.start, bookmarkB.start ) || !CKEDITOR.tools.arrayCompare( bookmarkA.end, bookmarkB.end ) )
return false;
}
}
return true;
}
};
/**
* Main logic for the Redo/Undo feature.
*
* **Note:** This class is not accessible from the global scope.
*
* @private
* @class CKEDITOR.plugins.undo.UndoManager
* @constructor Creates an UndoManager class instance.
* @param {CKEDITOR.editor} editor
*/
function UndoManager( editor ) {
this.editor = editor;
// Reset the undo stack.
this.reset();
}
UndoManager.prototype = {
/**
* When `locked` property is not `null`, the undo manager is locked, so
* operations like `save` or `update` are forbidden.
*
* The manager can be locked/unlocked by the {@link #lock} and {@link #unlock} methods.
*
* @private
* @property {Object} [locked=null]
*/
/**
* Handles keystroke support for the undo manager. It is called whenever a keystroke that
* can change the editor contents is pressed.
*
* @param {Number} keystroke The key code.
* @param {Boolean} isCharacter If `true`, it is a character ('a', '1', '&', ...). Otherwise it is the remove key (*Delete* or *Backspace*).
*/
type: function( keystroke, isCharacter ) {
// Create undo snap for every different modifier key.
var modifierSnapshot = ( !isCharacter && keystroke != this.lastKeystroke );
// Create undo snap on the following cases:
// 1. Just start to type .
// 2. Typing some content after a modifier.
// 3. Typing some content after make a visible selection.
var startedTyping = !this.typing || ( isCharacter && !this.wasCharacter );
var editor = this.editor;
if ( startedTyping || modifierSnapshot ) {
var beforeTypeImage = new Image( editor ),
beforeTypeCount = this.snapshots.length;
// Use setTimeout, so we give the necessary time to the
// browser to insert the character into the DOM.
CKEDITOR.tools.setTimeout( function() {
var currentSnapshot = editor.getSnapshot();
// In IE, we need to remove the expando attributes.
if ( CKEDITOR.env.ie )
currentSnapshot = currentSnapshot.replace( /\s+data-cke-expando=".*?"/g, '' );
// If changes have taken place, while not been captured yet (#8459),
// compensate the snapshot.
if ( beforeTypeImage.contents != currentSnapshot && beforeTypeCount == this.snapshots.length ) {
// It's safe to now indicate typing state.
this.typing = true;
// This's a special save, with specified snapshot
// and without auto 'fireChange'.
if ( !this.save( false, beforeTypeImage, false ) )
// Drop future snapshots.
this.snapshots.splice( this.index + 1, this.snapshots.length - this.index - 1 );
this.hasUndo = true;
this.hasRedo = false;
this.typesCount = 1;
this.modifiersCount = 1;
this.onChange();
}
}, 0, this );
}
this.lastKeystroke = keystroke;
this.wasCharacter = isCharacter;
// Create undo snap after typed too much (over 25 times).
if ( !isCharacter ) {
this.typesCount = 0;
this.modifiersCount++;
if ( this.modifiersCount > 25 ) {
this.save( false, null, false );
this.modifiersCount = 1;
} else {
setTimeout( function() {
editor.fire( 'change' );
}, 0 );
}
} else {
this.modifiersCount = 0;
this.typesCount++;
if ( this.typesCount > 25 ) {
this.save( false, null, false );
this.typesCount = 1;
} else {
setTimeout( function() {
editor.fire( 'change' );
}, 0 );
}
}
},
/**
* Resets the undo stack.
*/
reset: function() {
// Remember last pressed key.
this.lastKeystroke = 0;
// Stack for all the undo and redo snapshots, they're always created/removed
// in consistency.
this.snapshots = [];
// Current snapshot history index.
this.index = -1;
this.limit = this.editor.config.undoStackSize || 20;
this.currentImage = null;
this.hasUndo = false;
this.hasRedo = false;
this.locked = null;
this.resetType();
},
/**
* Resets all typing variables.
*
* @see #type
*/
resetType: function() {
this.typing = false;
delete this.lastKeystroke;
this.typesCount = 0;
this.modifiersCount = 0;
},
fireChange: function() {
this.hasUndo = !!this.getNextImage( true );
this.hasRedo = !!this.getNextImage( false );
// Reset typing
this.resetType();
this.onChange();
},
/**
* Saves a snapshot of the document image for later retrieval.
*/
save: function( onContentOnly, image, autoFireChange ) {
// Do not change snapshots stack when locked.
if ( this.locked )
return false;
var snapshots = this.snapshots;
// Get a content image.
if ( !image )
image = new Image( this.editor );
// Do nothing if it was not possible to retrieve an image.
if ( image.contents === false )
return false;
// Check if this is a duplicate. In such case, do nothing.
if ( this.currentImage ) {
if ( image.equalsContent( this.currentImage ) ) {
if ( onContentOnly )
return false;
if ( image.equalsSelection( this.currentImage ) )
return false;
} else
this.editor.fire( 'change' );
}
// Drop future snapshots.
snapshots.splice( this.index + 1, snapshots.length - this.index - 1 );
// If we have reached the limit, remove the oldest one.
if ( snapshots.length == this.limit )
snapshots.shift();
// Add the new image, updating the current index.
this.index = snapshots.push( image ) - 1;
this.currentImage = image;
if ( autoFireChange !== false )
this.fireChange();
return true;
},
restoreImage: function( image ) {
// Bring editor focused to restore selection.
var editor = this.editor,
sel;
if ( image.bookmarks ) {
editor.focus();
// Retrieve the selection beforehand. (#8324)
sel = editor.getSelection();
}
// Start transaction - do not allow any mutations to the
// snapshots stack done when selecting bookmarks (much probably
// by selectionChange listener).
this.locked = 1;
this.editor.loadSnapshot( image.contents );
if ( image.bookmarks )
sel.selectBookmarks( image.bookmarks );
else if ( CKEDITOR.env.ie ) {
// IE BUG: If I don't set the selection to *somewhere* after setting
// document contents, then IE would create an empty paragraph at the bottom
// the next time the document is modified.
var $range = this.editor.document.getBody().$.createTextRange();
$range.collapse( true );
$range.select();
}
this.locked = 0;
this.index = image.index;
this.currentImage = this.snapshots[ this.index ];
// Update current image with the actual editor
// content, since actualy content may differ from
// the original snapshot due to dom change. (#4622)
this.update();
this.fireChange();
editor.fire( 'change' );
},
// Get the closest available image.
getNextImage: function( isUndo ) {
var snapshots = this.snapshots,
currentImage = this.currentImage,
image, i;
if ( currentImage ) {
if ( isUndo ) {
for ( i = this.index - 1; i >= 0; i-- ) {
image = snapshots[ i ];
if ( !currentImage.equalsContent( image ) ) {
image.index = i;
return image;
}
}
} else {
for ( i = this.index + 1; i < snapshots.length; i++ ) {
image = snapshots[ i ];
if ( !currentImage.equalsContent( image ) ) {
image.index = i;
return image;
}
}
}
}
return null;
},
/**
* Checks the current redo state.
*
* @returns {Boolean} Whether the document has a previous state to retrieve.
*/
redoable: function() {
return this.enabled && this.hasRedo;
},
/**
* Checks the current undo state.
*
* @returns {Boolean} Whether the document has a future state to restore.
*/
undoable: function() {
return this.enabled && this.hasUndo;
},
/**
* Performs undo on current index.
*/
undo: function() {
if ( this.undoable() ) {
this.save( true );
var image = this.getNextImage( true );
if ( image )
return this.restoreImage( image ), true;
}
return false;
},
/**
* Performs redo on current index.
*/
redo: function() {
if ( this.redoable() ) {
// Try to save. If no changes have been made, the redo stack
// will not change, so it will still be redoable.
this.save( true );
// If instead we had changes, we can't redo anymore.
if ( this.redoable() ) {
var image = this.getNextImage( false );
if ( image )
return this.restoreImage( image ), true;
}
}
return false;
},
/**
* Updates the last snapshot of the undo stack with the current editor content.
*
* @param {CKEDITOR.plugins.undo.Image} [newImage] The image which will replace the current one.
* If not set defaults to image taken from editor.
*/
update: function( newImage ) {
// Do not change snapshots stack is locked.
if ( this.locked )
return;
if ( !newImage )
newImage = new Image( this.editor );
var i = this.index,
snapshots = this.snapshots;
// Find all previous snapshots made for the same content (which differ
// only by selection) and replace all of them with the current image.
while ( i > 0 && this.currentImage.equalsContent( snapshots[ i - 1 ] ) )
i -= 1;
snapshots.splice( i, this.index - i + 1, newImage );
this.index = i;
this.currentImage = newImage;
},
/**
* Locks the snapshot stack to prevent any save/update operations and when necessary,
* updates the tip of the snapshot stack with the DOM changes introduced during the
* locked period, after the {@link #unlock} method is called.
*
* It is mainly used to ensure any DOM operations that should not be recorded
* (e.g. auto paragraphing) are not added to the stack.
*
* **Note:** For every `lock` call you must call {@link #unlock} once to unlock the undo manager.
*
* @since 4.0
* @param {Boolean} [dontUpdate] When set to `true`, the last snapshot will not be updated
* with current contents and selection. By default, if undo manager was up to date when the lock started,
* the last snapshot will be updated to the current state when unlocking. This means that all changes
* done during the lock will be merged into the previous snapshot or the next one. Use this option to gain
* more control over this behavior. For example, it is possible to group changes done during the lock into
* a separate snapshot.
*/
lock: function( dontUpdate ) {
if ( !this.locked ) {
if ( dontUpdate )
this.locked = { level: 1 };
else {
// Make a contents image. Don't include bookmarks, because:
// * we don't compare them,
// * there's a chance that DOM has been changed since
// locked (e.g. fake) selection was made, so createBookmark2 could fail.
// http://dev.ckeditor.com/ticket/11027#comment:3
var imageBefore = new Image( this.editor, true );
// If current editor content matches the tip of snapshot stack,
// the stack tip must be updated by unlock, to include any changes made
// during this period.
var matchedTip = this.currentImage && this.currentImage.equalsContent( imageBefore );
this.locked = { update: matchedTip ? imageBefore : null, level: 1 };
}
}
// Increase the level of lock.
else
this.locked.level++;
},
/**
* Unlocks the snapshot stack and checks to amend the last snapshot.
*
* See {@link #lock} for more details.
*
* @since 4.0
*/
unlock: function() {
if ( this.locked ) {
// Decrease level of lock and check if equals 0, what means that undoM is completely unlocked.
if ( !--this.locked.level ) {
var updateImage = this.locked.update,
newImage = updateImage && new Image( this.editor, true );
this.locked = null;
if ( updateImage && !updateImage.equalsContent( newImage ) )
this.update();
}
}
}
};
} )();
/**
* The number of undo steps to be saved. The higher value is set, the more
* memory is used for it.
*
* config.undoStackSize = 50;
*
* @cfg {Number} [undoStackSize=20]
* @member CKEDITOR.config
*/
/**
* Fired when the editor is about to save an undo snapshot. This event can be
* fired by plugins and customizations to make the editor save undo snapshots.
*
* @event saveSnapshot
* @member CKEDITOR.editor
* @param {CKEDITOR.editor} editor This editor instance.
*/
/**
* Fired before an undo image is to be taken. An undo image represents the
* editor state at some point. It is saved into the undo store, so the editor is
* able to recover the editor state on undo and redo operations.
*
* @since 3.5.3
* @event beforeUndoImage
* @member CKEDITOR.editor
* @param {CKEDITOR.editor} editor This editor instance.
* @see CKEDITOR.editor#afterUndoImage
*/
/**
* Fired after an undo image is taken. An undo image represents the
* editor state at some point. It is saved into the undo store, so the editor is
* able to recover the editor state on undo and redo operations.
*
* @since 3.5.3
* @event afterUndoImage
* @member CKEDITOR.editor
* @param {CKEDITOR.editor} editor This editor instance.
* @see CKEDITOR.editor#beforeUndoImage
*/
/**
* Fired when the content of the editor is changed.
*
* Due to performance reasons, it is not verified if the content really changed.
* The editor instead watches several editing actions that usually result in
* changes. This event may thus in some cases be fired when no changes happen
* or may even get fired twice.
*
* If it is important not to get the change event too often, you should compare the
* previous and the current editor content inside the event listener.
*
* @since 4.2
* @event change
* @member CKEDITOR.editor
* @param {CKEDITOR.editor} editor This editor instance.
*/

View File

@ -0,0 +1,653 @@
/**
* @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
/**
* @fileOverview The "wysiwygarea" plugin. It registers the "wysiwyg" editing
* mode, which handles the main editing area space.
*/
( function() {
CKEDITOR.plugins.add( 'wysiwygarea', {
init: function( editor ) {
if ( editor.config.fullPage ) {
editor.addFeature( {
allowedContent: 'html head title; style [media,type]; body (*)[id]; meta link [*]',
requiredContent: 'body'
} );
}
editor.addMode( 'wysiwyg', function( callback ) {
var src = 'document.open();' +
// In IE, the document domain must be set any time we call document.open().
( CKEDITOR.env.ie ? '(' + CKEDITOR.tools.fixDomain + ')();' : '' ) +
'document.close();';
// With IE, the custom domain has to be taken care at first,
// for other browers, the 'src' attribute should be left empty to
// trigger iframe's 'load' event.
src = CKEDITOR.env.air ? 'javascript:void(0)' : CKEDITOR.env.ie ? 'javascript:void(function(){' + encodeURIComponent( src ) + '}())'
:
'';
var iframe = CKEDITOR.dom.element.createFromHtml( '<iframe src="' + src + '" frameBorder="0"></iframe>' );
iframe.setStyles( { width: '100%', height: '100%' } );
iframe.addClass( 'cke_wysiwyg_frame cke_reset' );
var contentSpace = editor.ui.space( 'contents' );
contentSpace.append( iframe );
// Asynchronous iframe loading is only required in IE>8 and Gecko (other reasons probably).
// Do not use it on WebKit as it'll break the browser-back navigation.
var useOnloadEvent = CKEDITOR.env.ie || CKEDITOR.env.gecko;
if ( useOnloadEvent )
iframe.on( 'load', onLoad );
var frameLabel = editor.title,
frameDesc = editor.lang.common.editorHelp;
if ( frameLabel ) {
if ( CKEDITOR.env.ie )
frameLabel += ', ' + frameDesc;
iframe.setAttribute( 'title', frameLabel );
}
var labelId = CKEDITOR.tools.getNextId(),
desc = CKEDITOR.dom.element.createFromHtml( '<span id="' + labelId + '" class="cke_voice_label">' + frameDesc + '</span>' );
contentSpace.append( desc, 1 );
// Remove the ARIA description.
editor.on( 'beforeModeUnload', function( evt ) {
evt.removeListener();
desc.remove();
} );
iframe.setAttributes( {
'aria-describedby': labelId,
tabIndex: editor.tabIndex,
allowTransparency: 'true'
} );
// Execute onLoad manually for all non IE||Gecko browsers.
!useOnloadEvent && onLoad();
if ( CKEDITOR.env.webkit ) {
// Webkit: iframe size doesn't auto fit well. (#7360)
var onResize = function() {
// Hide the iframe to get real size of the holder. (#8941)
contentSpace.setStyle( 'width', '100%' );
iframe.hide();
iframe.setSize( 'width', contentSpace.getSize( 'width' ) );
contentSpace.removeStyle( 'width' );
iframe.show();
};
iframe.setCustomData( 'onResize', onResize );
CKEDITOR.document.getWindow().on( 'resize', onResize );
}
editor.fire( 'ariaWidget', iframe );
function onLoad( evt ) {
evt && evt.removeListener();
editor.editable( new framedWysiwyg( editor, iframe.$.contentWindow.document.body ) );
editor.setData( editor.getData( 1 ), callback );
}
} );
}
} );
function onDomReady( win ) {
var editor = this.editor,
doc = win.document,
body = doc.body;
// Remove helper scripts from the DOM.
var script = doc.getElementById( 'cke_actscrpt' );
script && script.parentNode.removeChild( script );
script = doc.getElementById( 'cke_shimscrpt' );
script && script.parentNode.removeChild( script );
if ( CKEDITOR.env.gecko ) {
// Force Gecko to change contentEditable from false to true on domReady
// (because it's previously set to true on iframe's body creation).
// Otherwise del/backspace and some other editable features will be broken in Fx <4
// See: #107 and https://bugzilla.mozilla.org/show_bug.cgi?id=440916
body.contentEditable = false;
// Remove any leading <br> which is between the <body> and the comment.
// This one fixes Firefox 3.6 bug: the browser inserts a leading <br>
// on document.write if the body has contenteditable="true".
if ( CKEDITOR.env.version < 20000 ) {
body.innerHTML = body.innerHTML.replace( /^.*<!-- cke-content-start -->/, '' );
// The above hack messes up the selection in FF36.
// To clean this up, manually select collapsed range that
// starts within the body.
setTimeout( function() {
var range = new CKEDITOR.dom.range( new CKEDITOR.dom.document( doc ) );
range.setStart( new CKEDITOR.dom.node( body ), 0 );
editor.getSelection().selectRanges( [ range ] );
}, 0 );
}
}
body.contentEditable = true;
if ( CKEDITOR.env.ie ) {
// Don't display the focus border.
body.hideFocus = true;
// Disable and re-enable the body to avoid IE from
// taking the editing focus at startup. (#141 / #523)
body.disabled = true;
body.removeAttribute( 'disabled' );
}
delete this._.isLoadingData;
// Play the magic to alter element reference to the reloaded one.
this.$ = body;
doc = new CKEDITOR.dom.document( doc );
this.setup();
if ( CKEDITOR.env.ie ) {
doc.getDocumentElement().addClass( doc.$.compatMode );
// Prevent IE from leaving new paragraph after deleting all contents in body. (#6966)
editor.config.enterMode != CKEDITOR.ENTER_P && this.attachListener( doc, 'selectionchange', function() {
var body = doc.getBody(),
sel = editor.getSelection(),
range = sel && sel.getRanges()[ 0 ];
if ( range && body.getHtml().match( /^<p>(?:&nbsp;|<br>)<\/p>$/i ) && range.startContainer.equals( body ) ) {
// Avoid the ambiguity from a real user cursor position.
setTimeout( function() {
range = editor.getSelection().getRanges()[ 0 ];
if ( !range.startContainer.equals( 'body' ) ) {
body.getFirst().remove( 1 );
range.moveToElementEditEnd( body );
range.select();
}
}, 0 );
}
} );
}
// Fix problem with cursor not appearing in Webkit and IE11+ when clicking below the body (#10945, #10906).
// Fix for older IEs (8-10 and QM) is placed inside selection.js.
if ( CKEDITOR.env.webkit || ( CKEDITOR.env.ie && CKEDITOR.env.version > 10 ) ) {
doc.getDocumentElement().on( 'mousedown', function( evt ) {
if ( evt.data.getTarget().is( 'html' ) ) {
// IE needs this timeout. Webkit does not, but it does not cause problems too.
setTimeout( function() {
editor.editable().focus();
} );
}
} );
}
// ## START : disableNativeTableHandles and disableObjectResizing settings.
// Enable dragging of position:absolute elements in IE.
try {
editor.document.$.execCommand( '2D-position', false, true );
} catch ( e ) {}
// IE, Opera and Safari may not support it and throw errors.
try {
editor.document.$.execCommand( 'enableInlineTableEditing', false, !editor.config.disableNativeTableHandles );
} catch ( e ) {}
if ( editor.config.disableObjectResizing ) {
try {
this.getDocument().$.execCommand( 'enableObjectResizing', false, false );
} catch ( e ) {
// For browsers in which the above method failed, we can cancel the resizing on the fly (#4208)
this.attachListener( this, CKEDITOR.env.ie ? 'resizestart' : 'resize', function( evt ) {
evt.data.preventDefault();
} );
}
}
if ( CKEDITOR.env.gecko || CKEDITOR.env.ie && editor.document.$.compatMode == 'CSS1Compat' ) {
this.attachListener( this, 'keydown', function( evt ) {
var keyCode = evt.data.getKeystroke();
// PageUp OR PageDown
if ( keyCode == 33 || keyCode == 34 ) {
// PageUp/PageDown scrolling is broken in document
// with standard doctype, manually fix it. (#4736)
if ( CKEDITOR.env.ie ) {
setTimeout( function() {
editor.getSelection().scrollIntoView();
}, 0 );
}
// Page up/down cause editor selection to leak
// outside of editable thus we try to intercept
// the behavior, while it affects only happen
// when editor contents are not overflowed. (#7955)
else if ( editor.window.$.innerHeight > this.$.offsetHeight ) {
var range = editor.createRange();
range[ keyCode == 33 ? 'moveToElementEditStart' : 'moveToElementEditEnd' ]( this );
range.select();
evt.data.preventDefault();
}
}
} );
}
if ( CKEDITOR.env.ie ) {
// [IE] Iframe will still keep the selection when blurred, if
// focus is moved onto a non-editing host, e.g. link or button, but
// it becomes a problem for the object type selection, since the resizer
// handler attached on it will mark other part of the UI, especially
// for the dialog. (#8157)
// [IE<8 & Opera] Even worse For old IEs, the cursor will not vanish even if
// the selection has been moved to another text input in some cases. (#4716)
//
// Now the range restore is disabled, so we simply force IE to clean
// up the selection before blur.
this.attachListener( doc, 'blur', function() {
// Error proof when the editor is not visible. (#6375)
try {
doc.$.selection.empty();
} catch ( er ) {}
} );
}
// ## END
var title = editor.document.getElementsByTag( 'title' ).getItem( 0 );
title.data( 'cke-title', editor.document.$.title );
// [IE] JAWS will not recognize the aria label we used on the iframe
// unless the frame window title string is used as the voice label,
// backup the original one and restore it on output.
if ( CKEDITOR.env.ie )
editor.document.$.title = this._.docTitle;
CKEDITOR.tools.setTimeout( function() {
editor.fire( 'contentDom' );
if ( this._.isPendingFocus ) {
editor.focus();
this._.isPendingFocus = false;
}
setTimeout( function() {
editor.fire( 'dataReady' );
}, 0 );
// IE BUG: IE might have rendered the iframe with invisible contents.
// (#3623). Push some inconsequential CSS style changes to force IE to
// refresh it.
//
// Also, for some unknown reasons, short timeouts (e.g. 100ms) do not
// fix the problem. :(
if ( CKEDITOR.env.ie ) {
setTimeout( function() {
if ( editor.document ) {
var $body = editor.document.$.body;
$body.runtimeStyle.marginBottom = '0px';
$body.runtimeStyle.marginBottom = '';
}
}, 1000 );
}
}, 0, this );
}
var framedWysiwyg = CKEDITOR.tools.createClass( {
$: function( editor ) {
this.base.apply( this, arguments );
this._.frameLoadedHandler = CKEDITOR.tools.addFunction( function( win ) {
// Avoid opening design mode in a frame window thread,
// which will cause host page scrolling.(#4397)
CKEDITOR.tools.setTimeout( onDomReady, 0, this, win );
}, this );
this._.docTitle = this.getWindow().getFrame().getAttribute( 'title' );
},
base: CKEDITOR.editable,
proto: {
setData: function( data, isSnapshot ) {
var editor = this.editor;
if ( isSnapshot ) {
this.setHtml( data );
// Fire dataReady for the consistency with inline editors
// and because it makes sense. (#10370)
editor.fire( 'dataReady' );
}
else {
this._.isLoadingData = true;
editor._.dataStore = { id: 1 };
var config = editor.config,
fullPage = config.fullPage,
docType = config.docType;
// Build the additional stuff to be included into <head>.
var headExtra = CKEDITOR.tools.buildStyleHtml( iframeCssFixes() )
.replace( /<style>/, '<style data-cke-temp="1">' );
if ( !fullPage )
headExtra += CKEDITOR.tools.buildStyleHtml( editor.config.contentsCss );
var baseTag = config.baseHref ? '<base href="' + config.baseHref + '" data-cke-temp="1" />' : '';
if ( fullPage ) {
// Search and sweep out the doctype declaration.
data = data.replace( /<!DOCTYPE[^>]*>/i, function( match ) {
editor.docType = docType = match;
return '';
} ).replace( /<\?xml\s[^\?]*\?>/i, function( match ) {
editor.xmlDeclaration = match;
return '';
} );
}
// Get the HTML version of the data.
data = editor.dataProcessor.toHtml( data );
if ( fullPage ) {
// Check if the <body> tag is available.
if ( !( /<body[\s|>]/ ).test( data ) )
data = '<body>' + data;
// Check if the <html> tag is available.
if ( !( /<html[\s|>]/ ).test( data ) )
data = '<html>' + data + '</html>';
// Check if the <head> tag is available.
if ( !( /<head[\s|>]/ ).test( data ) )
data = data.replace( /<html[^>]*>/, '$&<head><title></title></head>' );
else if ( !( /<title[\s|>]/ ).test( data ) )
data = data.replace( /<head[^>]*>/, '$&<title></title>' );
// The base must be the first tag in the HEAD, e.g. to get relative
// links on styles.
baseTag && ( data = data.replace( /<head>/, '$&' + baseTag ) );
// Inject the extra stuff into <head>.
// Attention: do not change it before testing it well. (V2)
// This is tricky... if the head ends with <meta ... content type>,
// Firefox will break. But, it works if we place our extra stuff as
// the last elements in the HEAD.
data = data.replace( /<\/head\s*>/, headExtra + '$&' );
// Add the DOCTYPE back to it.
data = docType + data;
} else {
data = config.docType +
'<html dir="' + config.contentsLangDirection + '"' +
' lang="' + ( config.contentsLanguage || editor.langCode ) + '">' +
'<head>' +
'<title>' + this._.docTitle + '</title>' +
baseTag +
headExtra +
'</head>' +
'<body' + ( config.bodyId ? ' id="' + config.bodyId + '"' : '' ) +
( config.bodyClass ? ' class="' + config.bodyClass + '"' : '' ) +
'>' +
data +
'</body>' +
'</html>';
}
if ( CKEDITOR.env.gecko ) {
// Hack to make Fx put cursor at the start of doc on fresh focus.
data = data.replace( /<body/, '<body contenteditable="true" ' );
// Another hack which is used by onDomReady to remove a leading
// <br> which is inserted by Firefox 3.6 when document.write is called.
// This additional <br> is present because of contenteditable="true"
if ( CKEDITOR.env.version < 20000 )
data = data.replace( /<body[^>]*>/, '$&<!-- cke-content-start -->' );
}
// The script that launches the bootstrap logic on 'domReady', so the document
// is fully editable even before the editing iframe is fully loaded (#4455).
var bootstrapCode =
'<script id="cke_actscrpt" type="text/javascript"' + ( CKEDITOR.env.ie ? ' defer="defer" ' : '' ) + '>' +
'var wasLoaded=0;' + // It must be always set to 0 as it remains as a window property.
'function onload(){' +
'if(!wasLoaded)' + // FF3.6 calls onload twice when editor.setData. Stop that.
'window.parent.CKEDITOR.tools.callFunction(' + this._.frameLoadedHandler + ',window);' +
'wasLoaded=1;' +
'}' +
( CKEDITOR.env.ie ? 'onload();' : 'document.addEventListener("DOMContentLoaded", onload, false );' ) +
'</script>';
// For IE<9 add support for HTML5's elements.
// Note: this code must not be deferred.
if ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 ) {
bootstrapCode +=
'<script id="cke_shimscrpt">' +
'window.parent.CKEDITOR.tools.enableHtml5Elements(document)' +
'</script>';
}
data = data.replace( /(?=\s*<\/(:?head)>)/, bootstrapCode );
// Current DOM will be deconstructed by document.write, cleanup required.
this.clearCustomData();
this.clearListeners();
editor.fire( 'contentDomUnload' );
var doc = this.getDocument();
// Work around Firefox bug - error prune when called from XUL (#320),
// defer it thanks to the async nature of this method.
try { doc.write( data ); } catch ( e ) {
setTimeout( function() { doc.write( data ); }, 0 );
}
}
},
getData: function( isSnapshot ) {
if ( isSnapshot )
return this.getHtml();
else {
var editor = this.editor,
config = editor.config,
fullPage = config.fullPage,
docType = fullPage && editor.docType,
xmlDeclaration = fullPage && editor.xmlDeclaration,
doc = this.getDocument();
var data = fullPage ? doc.getDocumentElement().getOuterHtml() : doc.getBody().getHtml();
// BR at the end of document is bogus node for Mozilla. (#5293).
// Prevent BRs from disappearing from the end of the content
// while enterMode is ENTER_BR (#10146).
if ( CKEDITOR.env.gecko && config.enterMode != CKEDITOR.ENTER_BR )
data = data.replace( /<br>(?=\s*(:?$|<\/body>))/, '' );
data = editor.dataProcessor.toDataFormat( data );
if ( xmlDeclaration )
data = xmlDeclaration + '\n' + data;
if ( docType )
data = docType + '\n' + data;
return data;
}
},
focus: function() {
if ( this._.isLoadingData )
this._.isPendingFocus = true;
else
framedWysiwyg.baseProto.focus.call( this );
},
detach: function() {
var editor = this.editor,
doc = editor.document,
iframe = editor.window.getFrame();
framedWysiwyg.baseProto.detach.call( this );
// Memory leak proof.
this.clearCustomData();
doc.getDocumentElement().clearCustomData();
iframe.clearCustomData();
CKEDITOR.tools.removeFunction( this._.frameLoadedHandler );
var onResize = iframe.removeCustomData( 'onResize' );
onResize && onResize.removeListener();
// IE BUG: When destroying editor DOM with the selection remains inside
// editing area would break IE7/8's selection system, we have to put the editing
// iframe offline first. (#3812 and #5441)
iframe.remove();
}
}
} );
// DOM modification here should not bother dirty flag.(#4385)
function restoreDirty( editor ) {
if ( !editor.checkDirty() )
setTimeout( function() {
editor.resetDirty();
}, 0 );
}
function iframeCssFixes() {
var css = [];
// IE>=8 stricts mode doesn't have 'contentEditable' in effect
// on element unless it has layout. (#5562)
if ( CKEDITOR.document.$.documentMode >= 8 ) {
css.push( 'html.CSS1Compat [contenteditable=false]{min-height:0 !important}' );
var selectors = [];
for ( var tag in CKEDITOR.dtd.$removeEmpty )
selectors.push( 'html.CSS1Compat ' + tag + '[contenteditable=false]' );
css.push( selectors.join( ',' ) + '{display:inline-block}' );
}
// Set the HTML style to 100% to have the text cursor in affect (#6341)
else if ( CKEDITOR.env.gecko ) {
css.push( 'html{height:100% !important}' );
css.push( 'img:-moz-broken{-moz-force-broken-image-icon:1;min-width:24px;min-height:24px}' );
}
// #6341: The text cursor must be set on the editor area.
// #6632: Avoid having "text" shape of cursor in IE7 scrollbars.
css.push( 'html{cursor:text;*cursor:auto}' );
// Use correct cursor for these elements
css.push( 'img,input,textarea{cursor:default}' );
return css.join( '\n' );
}
} )();
/**
* Disables the ability of resize objects (image and tables) in the editing area.
*
* config.disableObjectResizing = true;
*
* @cfg
* @member CKEDITOR.config
*/
CKEDITOR.config.disableObjectResizing = false;
/**
* Disables the "table tools" offered natively by the browser (currently
* Firefox only) to make quick table editing operations, like adding or
* deleting rows and columns.
*
* config.disableNativeTableHandles = false;
*
* @cfg
* @member CKEDITOR.config
*/
CKEDITOR.config.disableNativeTableHandles = true;
/**
* Disables the built-in words spell checker if browser provides one.
*
* **Note:** Although word suggestions provided by browsers (natively) will
* not appear in CKEditor's default context menu,
* users can always reach the native context menu by holding the
* *Ctrl* key when right-clicking if {@link #browserContextMenuOnCtrl}
* is enabled or you're simply not using the context menu plugin.
*
* config.disableNativeSpellChecker = false;
*
* @cfg
* @member CKEDITOR.config
*/
CKEDITOR.config.disableNativeSpellChecker = true;
/**
* The CSS file(s) to be used to apply style to the contents. It should
* reflect the CSS used in the final pages where the contents are to be
* used.
*
* config.contentsCss = '/css/mysitestyles.css';
* config.contentsCss = ['/css/mysitestyles.css', '/css/anotherfile.css'];
*
* @cfg {String/Array} [contentsCss=CKEDITOR.getUrl( 'contents.css' )]
* @member CKEDITOR.config
*/
CKEDITOR.config.contentsCss = CKEDITOR.getUrl( 'contents.css' );
/**
* Language code of the writting language which is used to author the editor
* contents.
*
* config.contentsLanguage = 'fr';
*
* @cfg {String} [contentsLanguage=same value with editor's UI language]
* @member CKEDITOR.config
*/
/**
* The base href URL used to resolve relative and absolute URLs in the
* editor content.
*
* config.baseHref = 'http://www.example.com/path/';
*
* @cfg {String} [baseHref='']
* @member CKEDITOR.config
*/
/**
* Whether automatically create wrapping blocks around inline contents inside document body,
* this helps to ensure the integrality of the block enter mode.
*
* **Note:** Changing the default value might introduce unpredictable usability issues.
*
* config.autoParagraph = false;
*
* @since 3.6
* @cfg {Boolean} [autoParagraph=true]
* @member CKEDITOR.config
*/
/**
* Fired when some elements are added to the document.
*
* @event ariaWidget
* @member CKEDITOR.editor
* @param {CKEDITOR.editor} editor This editor instance.
* @param {CKEDITOR.dom.element} data The element being added.
*/

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@ -0,0 +1,40 @@
"Kama" Skin
====================
"Kama" is the default skin of CKEditor 3.x.
It's been ported to CKEditor 4 and fully featured.
For more information about skins, please check the [CKEditor Skin SDK](http://docs.cksource.com/CKEditor_4.x/Skin_SDK)
documentation.
Directory Structure
-------------------
CSS parts:
- **editor.css**: the main CSS file. It's simply loading several other files, for easier maintenance,
- **mainui.css**: the file contains styles of entire editor outline structures,
- **toolbar.css**: the file contains styles of the editor toolbar space (top),
- **richcombo.css**: the file contains styles of the rich combo ui elements on toolbar,
- **panel.css**: the file contains styles of the rich combo drop-down, it's not loaded
until the first panel open up,
- **elementspath.css**: the file contains styles of the editor elements path bar (bottom),
- **menu.css**: the file contains styles of all editor menus including context menu and button drop-down,
it's not loaded until the first menu open up,
- **dialog.css**: the CSS files for the dialog UI, it's not loaded until the first dialog open,
- **reset.css**: the file defines the basis of style resets among all editor UI spaces,
- **preset.css**: the file defines the default styles of some UI elements reflecting the skin preference,
- **editor_XYZ.css** and **dialog_XYZ.css**: browser specific CSS hacks.
Other parts:
- **skin.js**: the only JavaScript part of the skin that registers the skin, its browser specific files and its icons and defines the Chameleon feature,
- **icons/**: contains all skin defined icons,
- **images/**: contains a fill general used images.
License
-------
Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
Licensed under the terms of any of the following licenses at your choice: [GPL](http://www.gnu.org/licenses/gpl.html), [LGPL](http://www.gnu.org/licenses/lgpl.html) and [MPL](http://www.mozilla.org/MPL/MPL-1.1.html).
See LICENSE.md for more information.