Introduction
A few months ago, I had a situation in which I needed to build my own editor that will accept certain tags and attributes. I thought it would be very interesting to share how I built my editor in a browser independent way. To see a demo, click here.
Using the Code
The first thing that came to my mind then was what will be the container of my designer, an IFrame
, a div
, or what. Then I decided that I will make it a div
and I will set it's contentEditable attribute to true.
The second thing was OK now my div
could be edited by the end user in the design time, how will I apply the tags to the selected content. Below you will find a utility library I have built that has the basic miscellaneous methods we will need to build the editor. In the next part, I will show and explain the Editor
class. For now, let's start up with the important methods in the utility library.
GetSelectedRange : function(controlContent){
var selectedRange = null;
if(document.selection)
selectedRange = document.selection.createRange();
else if(window.selection)
selectedRange = window.selection.createRange();
if(selectedRange == null){
if(window.getSelection() != null)
{
if (this.Exists(window.getSelection().getRangeAt))
selectedRange = window.getSelection().getRangeAt(0);
else {
var range = document.createRange();
range.setStart(window.getSelection().anchorNode,
window.getSelection().anchorOffset);
range.setEnd(window.getSelection().focusNode,
window.getSelection().focusOffset);
selectedRange = range;
}
}
}
var t = null;
if(selectedRange != null && this.BrowserType.IE)
{
t = this.CheckParentById(controlContent.id,selectedRange.parentElement());
}
else{
t = this.RangeCompareNode(selectedRange,controlContent);
}
if(!t && controlContent != null)
selectedRange = null;
return selectedRange;
}
The above code gets the selected range object in a browser independent way in order to work on it later in the editor. Then it checks if the selected range is within our editor div
range. If it's not, then return null
otherwise return the range. Here we check if the range is within a parent node or not for browsers other than Internet Explorer.
RangeCompareNode : function(range,node){
var nodeRange = node.ownerDocument.createRange();
try {
nodeRange.selectNode(node);
}
catch (e) {
nodeRange.selectNodeContents(node);
}
var nodeIsBefore =
range.compareBoundaryPoints(Range.START_TO_START, nodeRange) == 1;
var nodeIsAfter = range.compareBoundaryPoints(Range.END_TO_END, nodeRange) == -1;
if (nodeIsBefore && !nodeIsAfter)
return false;
if (!nodeIsBefore && nodeIsAfter)
return false;
if (nodeIsBefore && nodeIsAfter)
return true;
return false;
}
Here we check if the range is within a parent node or not for Internet Explorer browser:
CheckParentById : function(parentId,child){
while(child != null){
if(child.id == parentId) return true;
child = child.parentNode;
}
return false;
}
Basically the editor is initialized inside RBMEditor
namespace. I will highlight some important methods in the editor. The Execute Command method first gets the current selected range then extracts the HTML from the range. After that it checks if the effect is already applied or not and if it is, it removes the effect using the ExecuteUndoCommand
method. If the effect isn't applied, then we create a new element from the command. Then the SetHTMLFromSelection
method will replace the current selected range with the new command element in the selected HTML.
ExecuteCommand: function(sCommand,id,value,name,isControl){
var selection = RBM.GetSelectedRange(this.controlContent);
if(selection == null)
return;
var html = this.GetHTMLFromSelection(selection);
var undo = this.ExecuteUndoCommand(sCommand,new String(html));
if(!undo){
var element = this.CreateNewNode(sCommand,id,value,html);
if(element != null)
this.SetHTMLFromSelection(selection,element);
}
}
The GET
HTML method will parse the HTML and code and remove the unsupported tags and attributes from the HTML code.
GetHTML: function(sCode){
var ts = new String(sCode);
var parts = ts.split("<");
var subpart = '';
var i = 0;
var j = 0;
var totalStr = '';
var tagName = '';
var readTag = true;
var readSub = true;
for(i = 0; i < parts.length;i++)
{
if(parts[i] == '')
continue;
subpart = '';
tagName = '';
readTag = true;
readSub = true;
for(j = 0; j < parts[i].length; j++)
{
if(parts[i].substr(j,1) == '>')
readSub = false;
if(parts[i].substr(j,1) == ' ' || parts[i].substr(j,1) == '>')
readTag = false;
if(readSub == true)
subpart = subpart + parts[i].substr(j,1);
if(readTag == true)
tagName = tagName + parts[i].substr(j,1);
}
if(this.IsSupportedTag(tagName) == false)
{
parts[i] = parts[i].replace(subpart + '>',' ');
parts[i] = parts[i].replace('/' + tagName + '>',' ');
}
else
{
parts[i] = '<' + parts[i];
parts[i] = this.RemoveUnknownAttributes
(subpart.replace(tagName,''),parts[i]);
}
}
var retValue = '';
for(i = 0; i < parts.length;i++)
{
if(parts[i] != '')
retValue = retValue + parts[i];
}
var tnewValue = new String(retValue);
return tnewValue;
}
So how to use the editor. First you need to initalize an object from the Editor
class and give it the div
id that you want to make it as an editor but to initialize it you need to put it this way:
var editor;
RBM.AddListener(window,'load',RbmInit);
function RbmInit(){
editor = new RBMEditor.Editor('oDiv')
}
Now to call a command simply type the following, you could change the BOLD Command with any of the commands listed in the EditorCommands
which you could find in the code or any other added command in it.
editor.ExecuteCommand(RBMEditor.EditorCommands.BOLD);
Finally hope that this post would help you to start building your own editor. Please feel free to ask me if you have any questions about more advanced techniques or more features like how to add a link, image, etc. I will try later to make separate posts for how to enhance the editor with such commands.
History
- 25th March, 2009: Initial post