FCKeditor Source Code Analysis (I) Chinese annotation analysis of fckeditor.js has been studying the source code of FCKeditor in the past few days (FCKeditor is a web editor with a wide range of applications in the network). I need to thank Nileaderblog for its hard translation.
I searched almost all the Internet, and it seems that I have explained a lot about the fckconfig.js file, but the information about the core FCK file of fckeditor.js is almost 0.
Therefore, I spent a whole day squeezing toothpaste to comment on the fckeditor.js core file, fckeditor.js, for reference by netizens who also learn fckeditor.
Given that the author's level is limited, here, please point out the inappropriate points in my comments to avoid misleading others. Thanks.
It is recommended to copy it to your IDE or
Note: This article is based on FCKeditor2.6.5
For more authoritative information, please refer to the FCK official Developers Guide
The code copy is as follows:
/**
*
**********CopyRight***************
*--------Annotated by nileader----
*----Version 1.00 2009-10-18----
*----Once copied, marked http://www.nileader.cn
*
* FCKeditor class annotated by nileader
* @param {Object} instanceName The unique name of the editor (equivalent to ID) is an unsaved parameter.
* width, height, toolbarset, value are all optional parameters
*/
var FCKeditor = function( instanceName, width, height, toolbarSet, value )
{
//Basic properties of the editor note: these things take precedence over the configuration in FCKConfig.js
this.InstanceName = instanceName; //The editor's unique name (equivalent to ID) (must have!)
this.Width = width || '100%' ; //The width is 100% by default
this.Height = height || '200' ; //The width is 200 by default
this.ToolbarSet = toolbarSet || 'Default' ;//The tool set name, the default value is Default
this.Value = value || '' ; //Initialize the HTML code of the editor, the default value is empty
//The default root path when the editor is initialized is to write fck. All paths used are defaulted to /Fckeditor/ from the FCKeditor.BasePath directory.
this.BasePath = FCKeditor.BasePath;
this.CheckBrowser = true; //Whether to check browser compatibility before displaying the editor, default is true
this.DisplayErrors = true; //Whether it is displayed with errors, the default is true
this.Config = new Object() ;
// Events
this.OnError = null ; // function( source, errorNumber, errorDescription ) custom error handling function
}
FCKeditor.BasePath = '/fckeditor/' ; // fck's default root directory
FCKeditor.MinHeight = 200; //The limits of height and width
FCKeditor.MinWidth = 750;
FCKeditor.prototype.Version = '2.6.5' ; //version number
FCKeditor.prototype.VersionBuild = '23959';
/**
* Call CreateHtml() to generate the editor's html code and output the editor on the page
*/
FCKeditor.prototype.Create = function()
{
//Calling the createhtml() method
document.write( this.CreateHtml() );
}
/**
* @return sHtml html code used to generate the editor
*/
FCKeditor.prototype.CreateHtml = function()
{
// Check if there is an InstanceName, no html code will be generated
if ( !this.InstanceName || this.InstanceName.length == 0 )
{
this._ThrowError( 701, 'You must specify an instance name.' ) ;
return '' ;
}
//Return value of function
var sHtml = '' ;
/*
* When the user's browser meets several preset browsers,
* Generate a text box with id=this.instancename name=this.instancename, the de facto content storage
*/
if ( !this.CheckBrowser || this._IsCompatibleBrowser() )
{
//Put this input after FCK initial value is escaped
sHtml += '<input type=hidden id=' + this.InstanceName + ' name=' + this.InstanceName + ' value=' + this._HTMLEncode( this.Value ) + ' style=display:none style=display:none />' ;
// Generate a hidden INPUT to place the contents in this.config
sHtml += this._GetConfigHtml();
//Code to generate the editor's iframe
sHtml += this._GetIFrameHtml();
}
/**
* If the user's browser is not compatible with the default FCK browsers
* Only traditional textareas can be found
*/
else
{
var sWidth = this.Width.toString().indexOf('%') > 0 ? this.Width : this.Width + 'px' ;
var sHeight = this.Height.toString().indexOf('%') > 0 ? this.Height : this.Height + 'px' ;
sHtml += '<textarea name=' + this.InstanceName +
' rows=4 cols=40 style=width:' + sWidth +
';height:' + sHeight ;
if ( this.TabIndex )
sHtml += ' tabindex=' + this.TabIndex;
sHtml += '>' +
this._HTMLEncode( this.Value ) +
'<//textarea>' ;
}
return sHtml;
}
/**
* Use the editor to replace the corresponding text box
*/
FCKeditor.prototype.ReplaceTextarea = function()
{
//If you already have the tag id=THIS.INSTANCENAME___Frame, return directly
if ( document.getElementById( this.InstanceName + '___Frame' ) )
return ;
//When the user's browser meets several preset browsers
if ( !this.CheckBrowser || this._IsCompatibleBrowser() )
{
// We must check the elements firstly using the Id and then the name.
//Get the html tag of id=this.InstanceName
var oTextarea = document.getElementById( this.InstanceName ) ;
//Get all the tags of name=THIS.instancename
var colElementsByName = document.getElementsByName( this.InstanceName );
var i = 0;
/*
* Considering that the naming of the user's html tag is not standardized, the following record is made to determine that the author refers to the user using name=this.instancename in the textarea tag.
* Name=this.instancename is also used on other tags on the same page
*/
while ( oTextarea || i == 0 )
{
//Travel until the textarea tag of name=this.instancename is found and assigned to oTextarea
if ( oTextarea && oTextarea.tagName.toLowerCase() == 'textarea' )
break;
oTextarea = colElementsByName[i++];
}
//If there is no tag with id or name of this.instancename, an error box pops up
if ( !oTextarea )
{
alert( 'Error: The TEXTAREA with id or name set to ' + this.InstanceName + ' was not found' ) ;
return ;
}
/*
* After confirming that the textarea tag with name=this.instancename exists, assign the editor's code to it
*/
oTextarea.style.display = 'none' ;
//If the tab key order is defined on the page for such textarea tags, assign it to this.TabIndex for later use
if ( oTextarea.tabIndex )
this.TabIndex = oTextarea.tabIndex;
this._InsertHtmlBefore( this._GetConfigHtml(), oTextarea );
this._InsertHtmlBefore( this._GetIFrameHtml(), oTextarea );
}
}
/**
* Insert html code in front of the specified page tag
* @param {Object} html code to be inserted
* @param {Object} Specified page tag (object)
*/
FCKeditor.prototype._InsertHtmlBefore = function(html, element)
{
if ( element.insertAdjacentHTML ) // IE private insertAdjacentHTML method
element.insertAdjacentHTML( 'beforeBegin', html );
else // non-ie browser
{
var oRange = document.createRange();
oRange.setStartBefore( element );
var oFragment = oRange.createContextualFragment(html);
element.parentNode.insertBefore( oFragment, element ) ;
}
}
/*
* Generate a hidden domain by editing this.Config[].
* For example:
* this.Config['nileader']=1104, this.Config['leaderni']=nichao...
* Then, sConfig=… &nileader=1104&leaderni=nichao…
* Of course, in the end, sConfig will be converted into a percentage encoding by the encodeURIComponent function and put into a hidden INPUT
*/
FCKeditor.prototype._GetConfigHtml = function()
{
var sConfig = '' ;
for ( var o in this.Config )
{
if ( sConfig.length > 0 ) sConfig += '&' ;
//The encodeURIComponent function is converted into percentage encoding
sConfig += encodeURIComponent( o ) + '=' + encodeURIComponent( this.Config[o] ) ;
}
return '<input type=hidden id=' + this.InstanceName + '___Config value=' + sConfig + ' style=display:none style=display:none />' ;
}
/*
* Generate the html of the iframe. Here it involves the determination of src
*/
FCKeditor.prototype._GetIFrameHtml = function()
{
var sFile = 'fckeditor.html';
//Special case, the window where fckedito is located is not embedded in the browser
try
{
if ( (/fcksource=true/i).test( window.top.location.search ) )
sFile = 'fckeditor.original.html';
}
catch (e) { /* Ignore this exception. Many times, the window where fckedito is located is embedded in the browser. */ }
/*
* One thing to note here:
* How iframe works: When the iframe is in editable state, the page where src is actually edited
* Here is a sLink to put it in the iframe tag
*/
//sLink is this de facto page, starting from the root directory of fck, for example, sLink=/fckeditor/editor/fckeditor.html?InstanceName=nileader&Toolbar=nileadersbar
var sLink = this.BasePath + 'editor/' + sFile + '?InstanceName=' + encodeURIComponent( this.InstanceName ) ;
if (this.ToolbarSet)
sLink += '&Toolbar=' + this.ToolbarSet ;
//Generate a real html code for editing iframer, of course, put src=slink
var html = '<iframe id=' + this.InstanceName +
'___Frame src=' + sLink +
' src=' + sLink +
' width=' + this.Width +
' height=' + this.Height ;
//If the traversal order using the Tab key is set, then assign it to the iframe
if ( this.TabIndex )
html += ' tabindex=' + this.TabIndex;
html += 'frameborder=0 scrolling=no></iframe>' ;
return html ;
}
/*
* Check whether the user's bowser is the default of fck
* This method is just FK company pursuing oo, meaningless
*/
FCKeditor.prototype._IsCompatibleBrowser = function()
{
return FCKeditor_IsCompatibleBrowser();
}
/**
* Error thrown
* @param {Object} errorNumber Error number
* @param {Object} errorDescription Error Overview
*/
FCKeditor.prototype._ThrowError = function( errorNumber, errorDescription )
{
this.ErrorNumber = errorNumber;
this.ErrorDescription = errorDescription;
//Whether it is displayed with errors, the default is true
if ( this.DisplayErrors )
{ //Print out the error number and error overview
document.write( '<div style=COLOR: #ff0000 style=COLOR: #ff0000>' ) ;
document.write( '[ FCKeditor Error ' + this.ErrorNumber + ': ' + this.ErrorDescription + ' ]' ) ;
document.write( '</div>' ) ;
}
//OnError Whether the error handling function is customized, if defined, it will be handled by it
if ( typeof( this.OnError ) == 'function' )
this.OnError( this, errorNumber, errorDescription ) ;
}
/**
* Escape text
* @param {Object} text to be escaped
* @return String text after escape
*/
FCKeditor.prototype._HTMLEncode = function( text )
{
if ( typeof( text ) != string )
text = text.toString();
//Substitute all & < > in the string with the corresponding escape characters
text = text.replace(
/&/g, &).replace(
//g, ).replace(
/</g, <).replace(
/>/g, >) ;
return text ;
}
;(function()
{
// Assign the textarea element on the page to the editor variable
var textareaToEditor = function( textarea )
{
var editor = new FCKeditor( textarea.name ) ;
editor.Width = Math.max( textarea.offsetWidth, FCKeditor.MinWidth ) ;
editor.Height = Math.max( textarea.offsetHeight, FCKeditor.MinHeight ) ;
return editor ;
}
/**
* Replace all <textarea> elements available in the document with FCKeditor
* instances.
*
* // Replace all <textarea> elements in the page.
* FCKeditor.ReplaceAllTextareas();
*
* // Replace all <textarea class=myClassName> elements in the page.
* FCKeditor.ReplaceAllTextareas( 'myClassName' ) ;
*
* // Selectively replace <textarea> elements, based on custom assertions.
* FCKeditor.ReplaceAllTextareas( function( textarea, editor )
* {
* // Custom code to evaluate the replace, returning false if it
* // must not be done.
* // It also passes the editor parameter, so the developer can
* // customize the instance.
* } ) ;
*/
FCKeditor.ReplaceAllTextareas = function()
{
//Get all textarea elements
var textareas = document.getElementsByTagName( 'textarea' ) ;
for ( var i = 0 ; i < textareas.length ; i++ )
{
var editor = null ;
var textarea = textareas[i] ;
var name = textarea.name ;
// The name attribute must exist.
if ( !name || name.length == 0 )
continue ;
if ( typeof arguments[0] == 'string' )
{
// The textarea class name could be passed as the function
// parameter.
var classRegex = new RegExp( '(?:^| )' + arguments[0] + '(?:$| )' ) ;
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>.
editor = textareaToEditor( textarea );
if ( arguments[0]( textarea, editor ) === false )
continue ;
}
if ( !editor )
editor = textareaToEditor( textarea );
editor.ReplaceTextarea();
}
}
})() ;
/**
* Detect browser compatibility
* Using some information sAgent returned by the navigator object, it determines that the browser returns information including the browser's code name, browser name, browser version language and other information and lowercase
* For example:
* mozilla/4.0 (compatible; msie 6.0; windows nt 5.2; sv1; .net clr 1.1.4322)
*
* When judging IE browser, the supported conditional compilation after using IE4.0 is added.
* Since it is only supported by IE, this property is not supported in W3C standard browsers. Therefore, the IE is judged appropriately by using this feature
*/
function FCKeditor_IsCompatibleBrowser()
{
var sAgent = navigator.userAgent.toLowerCase();
// The current browser is Internet Explorer 5.5+
//Use conditional compilation to judge IE In IE, /*@cc_on!@*/false == ! false == true,
//If it is a non-IE browser, ignore it, /*@cc_on!@*/false == false
if ( /*@cc_on!@*/false && sAgent.indexOf(mac) == -1 ) //Not apple mac os
{
var sBrowserVersion = navigator.appVersion.match(/MSIE (./..)/)[1] ;
return ( sBrowserVersion >= 5.5 ) ;
}
// Gecko (Opera 9 tries to behave like Gecko at this point).
//Detection whether it is OPERA 9 browser
if ( navigator.product == Gecko && navigator.productSub >= 20030210 && !( typeof(opera) == 'object' && opera.postError ) )
return true ;
// Opera 9.50+
if ( window.opera && window.opera.version && parseFloat( window.opera.version() ) >= 9.5 )
return true ;
// Adobe AIR
// Checked before Safari because AIR have the WebKit rich text editor
// features from Safari 3.0.4, but the version reported is 420.
if ( sAgent.indexOf( ' adobeair/' ) != -1 )
return ( sAgent.match( / adobeair//(/d+)/ )[1] >= 1 ) ; // Build must be at least v1
// Safari 3+
if ( sAgent.indexOf( ' applewebkit/' ) != -1 )
return ( sAgent.match( / applewebkit//(/d+)/ )[1] >= 522 ) ; // Build must be at least 522 (v3)
return false ;
}