SitePoint Sponsor

User Tag List

Results 1 to 16 of 16
  1. #1
    padawan silver trophybronze trophy markbrown4's Avatar
    Join Date
    Jul 2006
    Location
    Victoria, Australia
    Posts
    4,122
    Mentioned
    29 Post(s)
    Tagged
    2 Thread(s)

    Object Oriented Javascript and losign focus

    I think I may have asked this question before.. I must not have understood the answer because i'm asking the same questions without knowing an answer.

    I'm building a simple cross-browser rich text editor and the first iteration was working ok but I wanted to experiment with a more oo design so I have started on the second iteration

    The problems are pretty self evident if you look at editor.js in the second version. The problem comes when I am trying to link events to a specific objects function.

    How do you use Event listeners and keep track of objects?
    Or can you somehow gain a reference to an existing object?

    At the moment i'm initializing my objects like this:
    Code:
    var mbe = new MBEditor();
    mbe.init(<textarea's id>);
    Should I be naming this object <textarea's id>_mbe using the eval function so I can pick it up later from within an event listener?

    I'm quite confused with this one, hence the rambling description - I hope someone can make some sense of it

    Thanks again,

  2. #2
    SitePoint Wizard silver trophy kyberfabrikken's Avatar
    Join Date
    Jun 2004
    Location
    Copenhagen, Denmark
    Posts
    6,157
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    I'm not sure, I understand your question, but I'll make an attempt. I'm guessing, your problem is, how to make the toolbar buttons trigger a method on your javascript object, when clicked, right?

    You probably realise this already, but I'm just going to point it out to be sure; Javascript exists in one domain and the HTML document exists in a different one entirely. There are different ways, that the javascript code can interact with the HTML document. You can use document.write, you can use innerHTML and you can use the DOM API.

    The document can also interact with the javascript, through events. When an event happens, it may invoke a block of javascript code (a handler). There are 3 ways, that you can bind events; You can assign a function within javascript, you can use the event-registration API (Which, unfortunately, is complicated by the lack of standards support from IE) or you can use inline handlers. With the two first types (Of which, you should only use the latter btw.), you assign a function as the handler, so it can contain references to native javascript types (objects). With inline handlers, you can't contain native types.

    The simple answer then, is to avoid inline handlers and instead rely on event-registration. Since this presupposes that you have a reference to the element, which is raising the event (Eg. the A-node), you are better off using the DOM API, instead of innerHTML (As you are now). The DOM API is notorious for being verbose and in addition, it's a bit slower to execute, than using innerHTML. Regardless, it is the "right" thing to use, per default. You can use an abstraction library, to make the API more elegant to work with, but I'd recommend that you try working directly with it for starters; It's good to know how it works, before moving up to a higher abstraction.

    Your code can be rewritten to use the DOM API, like this:
    Code javascript:
     
        // Will create a callback, that invokes this.onfoo(), when the event foo is triggered.
        // We'll use this below
        var makeHandler = function(key, self) {
          var self = this;
          return function() { console.log(self, "on" + key); self["on" + key](); }
        }
     
        var elements = [];
        // Create Toolbar
        var ul = document.createElement("ul");
        elements.push(ul);
        ul.setAttribute("id", this.id + "_toolbar");
        ul.setAttribute("class", "mbe_toolbar");
        ul.setAttribute("style", "width: " + this.width + "px");
        this.Toolbar = ul;
     
        // Add Format Selection
        var li = ul.appendChild(document.createElement("li"));
        var select = li.appendChild(document.createElement("select"));
        select.setAttribute("class", "formatSelect");
        select.setAttribute("id", this.id + "_format");
        select.options.add(new Option("Format Selection", "", true));
        this.formatOptions.each(function(format) {
            select.options.add(new Option(format.value, format.key, false));
          });
        this.formatSelect = select;
     
        // Add Editor Tools
        this.tools.each(function(tool) {
            var li = ul.appendChild(document.createElement("li"));
            var a = li.appendChild(document.createElement("a"));
            a.setAttribute("title", tool.value);
            a.setAttribute("class", tool.key);
            a.appendChild(document.createTextNode(tool.value));
            Event.observe(a, 'click', makeHandler(tool.key));
          });
     
        // Add View Source Tools
        var ul = document.createElement("ul");
        elements.push(ul);
        ul.setAttribute("id", this.id + "_source_toolbar");
        ul.setAttribute("class", "mbe_toolbar");
        ul.setAttribute("style", "width: " + this.width + "px; display: none");
        this.srcToolbar = ul;
     
        this.sourceTools.each(function(tool) {
            var li = ul.appendChild(document.createElement("li"));
            var a = li.appendChild(document.createElement("a"));
            a.setAttribute("title", tool.value);
            a.setAttribute("class", tool.key);
            a.appendChild(document.createTextNode(tool.value));
            Event.observe(a, 'click', makeHandler(tool.key));
          });
     
        // Create iframe of same dimensions as textarea
        var iframe = document.createElement("iframe");
        elements.push(iframe);
        iframe.setAttribute("id", this.id + "_iframe");
        iframe.setAttribute("name", this.id + "_iframe");
        iframe.setAttribute("width", this.width + "px");
        iframe.setAttribute("height", this.height + "px");
        this.iframe = iframe;
     
        // Add toolbar and iframe before textarea
        elements.each(function(element) {
            textarea.parentNode.insertBefore(element, textarea);
          });
     
        /*
        // Create Toolbar
        var str = "<ul id=\"" + this.id + "_toolbar\" class=\"mbe_toolbar\" style=\"width: " + this.width + "px\">";
     
        // Add Format Selection
        str += "<li><select class=\"formatSelect\" id=\"" + this.id + "_format\"><option selected=\"selected\">Format Selection</option>";
        this.formatOptions.each(function(format) {
        str += "<option value=\"" + format.key + "\">" + format.value + "</option>";
        });
        str += "</select></li>";
     
        // Add Editor Tools
        this.tools.each(function(tool) {
        str += "<li><a title=\"" + tool.value + "\" class=\"" + tool.key + "\">" + tool.value + "</a></li>";
        });
        str += '</ul>';
     
        // Add View Source Tools
        str += "<ul id=\""+ this.id +"_source_toolbar\" class=\"mbe_toolbar\" style=\"width: " + this.width + "px; display: none\">";
        this.sourceTools.each(function(tool) {
        str += "<li><a title=\"" + tool.value + "\" class=\"" + tool.key + "\">" + tool.value + "</a></li>";
        });
        str += '</ul>';
        // Create iframe of same dimensions as textarea
        str += '<iframe id="' + this.id + '_iframe" name="' + this.id + '_iframe" width="' + this.width + 'px" height="' + this.height + 'px"></iframe>';
     
        // Add toolbar and iframe before textarea
        new Insertion.Before(this.textarea, str);
        */
     
        // Get elements in object that we just inserted into document
        this.textarea.hide();
        // this.iframe = $(this.id + '_iframe');
        // this.formatSelect = $(this.id + '_format');
        // this.Toolbar = $(this.id + '_toolbar');
        // this.srcToolbar = $(this.id + '_source_toolbar');
        this.editable = this.iframe.contentWindow.document;

  3. #3
    padawan silver trophybronze trophy markbrown4's Avatar
    Join Date
    Jul 2006
    Location
    Victoria, Australia
    Posts
    4,122
    Mentioned
    29 Post(s)
    Tagged
    2 Thread(s)
    kyberfabrikken,

    Wow, thanks for the great explanation, you've always been a faithful help to me on these forums.
    I've never used console.log before.. If I understand the makeHandler function correctly - I will need to write a onbold, onitalic etc.. function for each tool - and within that function because it is a function of the object I can have access to it's other properties and methods. If so that is exactly what I am looking for.

    I'll experiment with your code and see If I can make complete sense of it. Another thing I was wondering about was capturing key events on the editor and hooking up those events to it's own object. I imagine that could be done using the same method.

    Can you explain this function please,
    Code:
    var makeHandler = function(key, self) {
          var self = this;
          return function() { console.log(self, "on" + key); self["on" + key](); }
    }
    When you call this function you only give it the key - Is a reference to the object always sent as another parameter in every function?
    It seems like the first line within the function would override the parameter passed into the function also.
    At the moment I am getting the following error when clicking a button "self["on" + key] is not a function"

    Thanks,
    Last edited by markbrown4; Dec 26, 2007 at 17:18.

  4. #4
    SitePoint Wizard silver trophy kyberfabrikken's Avatar
    Join Date
    Jun 2004
    Location
    Copenhagen, Denmark
    Posts
    6,157
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Quote Originally Posted by markbrown4 View Post
    Can you explain this function please,
    Code:
    var makeHandler = function(key, self) {
          var self = this;
          return function() { console.log(self, "on" + key); self["on" + key](); }
    }
    When you call this function you only give it the key - Is a reference to the object always sent as another parameter in every function?
    It seems like the first line within the function would override the parameter passed into the function also.
    My bad. I changed the function, just before posting it here. To avoid confusion, change it to:
    Code:
    var makeHandler = function(key) {
      var self = this;
      return function() { console.log(self, "on" + key); self["on" + key](); }
    }
    Quote Originally Posted by markbrown4 View Post
    At the moment I am getting the following error when clicking a button "self["on" + key] is not a function"
    Yep, that's simply because you haven't defined the functions for handling the events yet. (Eg. onbold, onitalic etc.)

    I should also note, that console.log is part of Firebug, so it only works, when that's installed and running. But it looks like you figured that out already.

  5. #5
    padawan silver trophybronze trophy markbrown4's Avatar
    Join Date
    Jul 2006
    Location
    Victoria, Australia
    Posts
    4,122
    Mentioned
    29 Post(s)
    Tagged
    2 Thread(s)
    Quote Originally Posted by kyberfabrikken View Post
    Yep, that's simply because you haven't defined the functions for handling the events yet. (Eg. onbold, onitalic etc.)
    I've added the following to the MBEditor object but still no go:
    Code:
    this.onbold = function() {
    	alert('bold clicked');
    };
    Edit:
    When I define it as self.onbold the function is called - but I can't seem to access any of the properties or functions of the object still.
    I should also note, that console.log is part of Firebug, so it only works, when that's installed and running. But it looks like you figured that out already.
    Thanks, I didn't realize that.

    Thanks,

  6. #6
    padawan silver trophybronze trophy markbrown4's Avatar
    Join Date
    Jul 2006
    Location
    Victoria, Australia
    Posts
    4,122
    Mentioned
    29 Post(s)
    Tagged
    2 Thread(s)
    I thought of a solution that seems to work, let me know what you think of it.
    I've added a global array that holds references to the objects:
    Code:
    var mb_editors = [];
    And when i'm initializing the objects I am storing them in the global array:
    Code:
    var i = 0;
    rtes.each(function(textarea) {
    	// Replace textarea with editable iframe
    	mb_editors[i] = new MBEditor();
    	mb_editors[i].init(i, textarea);
    	i++;
    });
    Then when I register the events I connect them to the global reference:
    Code:
    Event.observe(a, 'click', function() { mb_editors[i].editorCommand(tool.key, '')});
    It doesn't seem all that neat and tidy - but I can't find a better way at the moment.

    Complete javascript
    Code JavaScript:
    var mb_editors = [];
     
    var MBEditor = function() {
     
    	this.editorNumber = 0;
    	this.id = false;
    	this.textarea = false;
    	this.iframe = false;
    	this.editable = false;
    	this.container = false;
    	this.toolbar = false;
    	this.srcToolbar = false;
    	this.formatSelect = false;
    	this.toolSelect = false;
     
    	// Create an array of hash groups with the default tools that use execCommand
     	this.tools = [];
    	this.tools.push($H({
    		bold: 'Bold',
    		italic: 'Italic'
    	}));
    	this.tools.push($H({
    		// underline: 'Underline',
    		justifyleft: 'Align Left',
    		justifycenter: 'Align Center',
    		justifyright: 'Align Right'
    	}));
    	this.tools.push($H({
    		insertorderedlist: 'Numbered List',
    		insertunorderedlist: 'Bulletpoint List',
    		outdent: 'Outdent',
    		indent: 'Indent'
    	}));
    	this.tools.push($H({
    		undo: 'Undo',
    		redo: 'Redo'
    	}));
     
    	// Tools for HTML editing
    	this.sourceTools = $H({
    		viewSource: 'View Editor'
    	});
     
    	// Different formatting options
    	this.formatOptions = $H({
    		P: 'Normal',
    		H1: 'Heading 1',
    		H2: 'Heading 2',
    		H3: 'Heading 3',
    		H4: 'Heading 4',
    		H5: 'Heading 5',
    		H6: 'Heading 6',
    	});
     
    	// Custom Tools
    	this.customTools = $H({
    		image: 'Insert Image',
    		table: 'Insert Table',
    		link: 'Insert Link',
    		anchor: 'Insert Anchor',
    		manageAnchors: 'Manage Anchors',
    		manageFiles: 'Manage Files',
    		viewSource: 'Edit HTML Source'
    	});
     
    	this.init = function(i, textarea) {
     
    		this.editorNumber = i;
    		this.textarea = textarea;
    		this.id = textarea.getAttribute('id');
    		var dim = textarea.getDimensions();
    		this.height = dim.height;
    		this.width = dim.width;
     
    		// Array to hold our elements to add into the document
    	    var elements = [];
     
    	    // Create Toolbar
    	    var ul = document.createElement("ul");
    	    elements.push(ul);
    	    ul.setAttribute("id", this.id + "_toolbar");
    	    ul.setAttribute("class", "mbe_toolbar");
    	    ul.setAttribute("style", "width: " + this.width + "px");
    	    this.toolbar = ul;
     
    	    // Add Format Selection
    	    var li = ul.appendChild(document.createElement("li"));
    	    var select = li.appendChild(document.createElement("select"));
    	    select.setAttribute("class", "formatSelect");
    	    select.setAttribute("id", this.id + "_format");
    		select.options.add(new Option('Format Selection', '', false));
    	    this.formatOptions.each(function(format) {
    	        select.options.add(new Option(format.value, format.key, false));
    	    });
    	    this.formatSelect = select;
    		Event.observe(select, 'change', function() { mb_editors[i].formatSelection()});
     
    	    // Add Editor Tools
    	    this.tools.each(function(toolGroup) {
    			var ulli = ul.appendChild(document.createElement("li"));
    			var ulul = ulli.appendChild(document.createElement("ul"));
    			toolGroup.each(function(tool) {		
    		        var lili = ulul.appendChild(document.createElement("li"));
    		        var a = lili.appendChild(document.createElement("a"));
    		        a.setAttribute("title", tool.value);
    		        a.setAttribute("class", tool.key);
    		        a.appendChild(document.createTextNode(tool.value));
    		        Event.observe(a, 'click', function() { mb_editors[i].editorCommand(tool.key, '')});
    			});
    	    });
     
    		// Add Custom Tools Selection
    	    var li = ul.appendChild(document.createElement("li"));
    	    var select = li.appendChild(document.createElement("select"));
    	    select.setAttribute("class", "customToolSelect");
    	    select.setAttribute("id", this.id + "_custom_tool");
    		select.options.add(new Option('Tools', '', false));
    	    this.customTools.each(function(tool) {
    	        select.options.add(new Option(tool.value, tool.key, false));
    	    });
    	    this.toolSelect = select;
    		Event.observe(select, 'change', function() { mb_editors[i].toolSelection()});
     
    	    // Add View Source Tools
    	    var ul = document.createElement("ul");
    	    elements.push(ul);
    	    ul.setAttribute("id", this.id + "_source_toolbar");
    	    ul.setAttribute("class", "mbe_toolbar");
    	    ul.setAttribute("style", "width: " + this.width + "px; display: none");
    	    this.srcToolbar = ul;
     
    	    this.sourceTools.each(function(tool) {
    	        var li = ul.appendChild(document.createElement("li"));
    	        var a = li.appendChild(document.createElement("a"));
    	        a.setAttribute("title", tool.value);
    	        a.setAttribute("class", tool.key);
    	        a.appendChild(document.createTextNode(tool.value));
    			Event.observe(a, 'click', function() { mb_editors[i].sourceToolSelection(tool.key)});
    	    });
     
    	    // Create iframe of same dimensions as textarea
    	    var iframe = document.createElement("iframe");
    	    elements.push(iframe);
    	    iframe.setAttribute("id", this.id + "_iframe");
    		iframe.setAttribute("class", "mbe_iframe");
    	    iframe.setAttribute("name", this.id + "_iframe");
    	    iframe.setAttribute("width", this.width + "px");
    	    iframe.setAttribute("height", this.height + "px");
    	    this.iframe = iframe;
     
    	    // Add toolbar and iframe before textarea
    	    elements.each(function(element) {
    	        textarea.parentNode.insertBefore(element, textarea);
    	    });
    		this.textarea.hide();
    		this.editable = this.iframe.contentWindow.document;
     
    		// write the content of textarea to the iframe, If textarea contains nothing then set to default
    		var content = (this.textarea.value == '') ? '<html><head></head><body></body></html>' : this.textarea.value;
    		this.editable.write(content);
    		this.editable.close();
     
    		// enable the designMode	
    		try {
    			this.editable.designMode =  "on" ;
    			this.iframe.contentDocument.designMode = "on";
    		}
    		catch(e) { /* alert(e); */ }
    	};
     
    	// assign the value of the iframe to the textarea
    	this.updateTextarea = function() {
    		this.textarea.value = this.editable.body.innerHTML;
    	};
     
    	// assign the value of the textarea to the iframe
    	this.updateIframe = function() {
    		this.editable.body.innerHTML = this.textarea.value;
    	};
     
    	// View Source
    	this.toggleSource = function() {
    		// Hide visible editor and show hidden editor
    		if (this.textarea.visible()) {
    			this.textarea.hide();
    			this.srcToolbar.hide();
    			this.toolbar.show();
    			this.iframe.show();
    			this.updateIframe();
    		}
    		else {
    			this.toolbar.hide();
    			this.iframe.hide();
    			this.textarea.show();
    			this.srcToolbar.show();
    			this.updateTextarea();
    		}
    	};
     
    	this.formatSelection = function() {
    		this.editorCommand('formatblock', '<'+this.formatSelect.options[this.formatSelect.selectedIndex].value+'>');
    		this.formatSelect.selectedIndex = 0;
    	};
     
    	this.toolSelection = function() {
    		var key = this.toolSelect.options[this.toolSelect.selectedIndex].value;
    		switch(key) {
     
    			// View HTML Source
    			case 'viewSource':
    				this.toggleSource();
    			break;
    			default:
    				alert('Sorry, The Tool "'+key+'" has not been implemented yet :(');
    			break;
    		}
     
    		this.toolSelect.selectedIndex = 0;
    	};
     
    	this.sourceToolSelection = function(key) {
    		switch(key) {
     
    			// View HTML Source
    			case 'viewSource':
    				this.toggleSource();
    			break;
    			default:
    				alert('Sorry, The Source Tool "'+key+'" has not been implemented yet :(');
    			break;
    		}
    	};
     
    	this.addContent = function(content) {
    		if (this.textarea.visible()) {
    			// Add content to textarea
    			this.textarea.value += content;
    		}
    		else {
    			// Add content to iframe
    			this.editable.write(content);
    		}
    	};
     
    	this.editorCommand = function(command, options) {
    		var rte = this.iframe.contentWindow;
    		try {
    			rte.focus();
    			rte.document.execCommand(command, false, options);
    			rte.focus();
    		} catch (e) { /* alert(e); */ }
    	};
    };
     
    // init method
    Event.observe(window, 'load', function() {
     
    	// textarea will only be added if browser supports designMode
    	if (document.designMode) {
     
    		// Get all textarea's in page with class="rte"
    		rtes = $$('textarea.mbe');
    		var i = 0;
    		rtes.each(function(textarea) {
    			// Replace textarea with editable iframe
    			mb_editors[i] = new MBEditor();
    			mb_editors[i].init(i, textarea);
    			i++;
    		});
    	}
    });
    Last edited by markbrown4; Dec 26, 2007 at 22:07.

  7. #7
    SitePoint Wizard silver trophy kyberfabrikken's Avatar
    Join Date
    Jun 2004
    Location
    Copenhagen, Denmark
    Posts
    6,157
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Quote Originally Posted by markbrown4 View Post
    When I define it as self.onbold the function is called - but I can't seem to access any of the properties or functions of the object still.
    Ah. Try replacing the function with this:
    Code:
    var makeHandler = function(key, subject) {
      return function() { console.log(subject, "on" + key); subject["on" + key](); }
    }
    And then replace calls to it, with this as second argument. Eg. makeHandler(tool.key, this)

    Quote Originally Posted by markbrown4 View Post
    I thought of a solution that seems to work, let me know what you think of it.
    I've added a global array that holds references to the objects:
    That's a pretty nasty solution. The other stuff should work.

  8. #8
    padawan silver trophybronze trophy markbrown4's Avatar
    Join Date
    Jul 2006
    Location
    Victoria, Australia
    Posts
    4,122
    Mentioned
    29 Post(s)
    Tagged
    2 Thread(s)
    Thanks again kyberfabrikken,

    I got your suggestion to work, just needed to add a local variable containing a reference to 'this' - Within prototype's iterators:
    Code:
    this.sourceTools.each(function(tool) { /* in here */ })
    'this' becomes a reference to 'window'.

    I feel I'm getting the hang of it now, but there's always things like this that trip me up in javascript land. Thanks very much for your help - It's taught me more than I expected it to, cheers.

    Edit:
    Sorry to be a bother
    But IE really doesn't like the DOM alternative, the styles aren't rendering properly - I'll put up a linky linky

  9. #9
    SitePoint Wizard silver trophy kyberfabrikken's Avatar
    Join Date
    Jun 2004
    Location
    Copenhagen, Denmark
    Posts
    6,157
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Quote Originally Posted by markbrown4 View Post
    I got your suggestion to work, just needed to add a local variable containing a reference to 'this' - Within prototype's iterators:
    Aha. That's why the other thing didn't work. I thought it was me, who was going insane. Prototype is really odd ...

    Quote Originally Posted by markbrown4 View Post
    But IE really doesn't like the DOM alternative, the styles aren't rendering properly - I'll put up a linky linky
    Yeah, IE has some quirks. Certain attributes can't be set with setAttribute(). Instead of this:
    Code:
    ul.setAttribute("style", "width: " + this.width + "px");
    Use this:
    Code:
    ul.style.width = this.width + "px";

  10. #10
    padawan silver trophybronze trophy markbrown4's Avatar
    Join Date
    Jul 2006
    Location
    Victoria, Australia
    Posts
    4,122
    Mentioned
    29 Post(s)
    Tagged
    2 Thread(s)
    nope

    linky linky

    Edit:
    It's the class setting that IE doesn't like,

    Changing elm.setAttribute("class", xxx) to elm.className = xxx does the trick.

    Now.. onto registering key events on the editors, i'll give it a decent shot before I hassle you further


    Thanks very much.

  11. #11
    I meant that to happen silver trophybronze trophy Raffles's Avatar
    Join Date
    Sep 2005
    Location
    Tanzania
    Posts
    4,662
    Mentioned
    2 Post(s)
    Tagged
    0 Thread(s)
    It's a bug with IE.

    elm.setAttribute('className', xxx) is stupidly what works in it.

  12. #12
    padawan silver trophybronze trophy markbrown4's Avatar
    Join Date
    Jul 2006
    Location
    Victoria, Australia
    Posts
    4,122
    Mentioned
    29 Post(s)
    Tagged
    2 Thread(s)
    Ahoi hoi A few more issues I can't seem to resolve..

    - In IE6 i'm getting a nasty permission denied error when trying to get the contents of the iframe. - 'View Source' from the Tools menu.
    - In all IE's i'm having a problem with the standard tools, i'm losing the selection and the command only effects the very first block in the editor. The drop down menu's don't seem to lose this focus. This does work in IE in the first iteration so I think it has something to do with the event listeners or perhaps prototypes iterators.
    - I also can't figure out how to get the keypress listeners to send the event as a parameter of the handler.

    Code javascript:
    /* This class initialises on load event of window
    * It replaces all textareas with a class of 'mbe' with a rich text editor
    **/
    var MBEditor = function() {
     
    	// Formatting Selection
    	this.formatOptions = $H({
    		P: 'Normal',
    		H1: 'Heading 1',
    		H2: 'Heading 2',
    		H3: 'Heading 3',
    		H4: 'Heading 4',
    		H5: 'Heading 5',
    		H6: 'Heading 6'
    	});
     
    	// Create an array of hash groups with the default tools that use execCommand
    	this.tools = [];
    	this.tools.push($H({
    		bold: 'Bold',
    		italic: 'Italic'
    	}));
    	this.tools.push($H({
    		// underline: 'Underline',
    		justifyleft: 'Align Left',
    		justifycenter: 'Align Center',
    		justifyright: 'Align Right'
    	}));
    	this.tools.push($H({
    		insertorderedlist: 'Numbered List',
    		insertunorderedlist: 'Bulletpoint List',
    		outdent: 'Outdent',
    		indent: 'Indent'
    	}));
    	this.tools.push($H({
    		undo: 'Undo',
    		redo: 'Redo'
    	}));
     
    	// Custom Tools
    	this.customTools = $H({
    		image: 'Insert Image',
    		table: 'Insert Table',
    		link: 'Insert Link',
    		anchor: 'Insert Anchor',
    		manageAnchors: 'Manage Anchors',
    		manageFiles: 'Manage Files',
    		toggleSource: 'Edit HTML Source'
    	});
     
    	// Tools for HTML editing
    	this.sourceTools = $H({
    		toggleSource: 'View Editor'
    	});
     
    	this.init = function(textarea) {
     
    		this.textarea = textarea;
    		this.id = textarea.getAttribute('id');
    		var dim = textarea.getDimensions();
    		this.height = dim.height;
    		this.width = dim.width;
     
    		// Reference to this object for event registration
    		var self = this;
     
    		// Closures for Event Registration
    		var makeHandler = function(subject, method) {
    			return function() { subject[method](); }
    		};
     
    		var makeCommandHandler = function(subject, cmd, opt) {
    			return function() { subject['editorCommand'](cmd, opt); }
    		};
     
    		// Array to hold our elements to add into the document
    		var elements = [];
     
    		// Create Toolbar
    		var ul = document.createElement("ul");
    		elements.push(ul);
    		ul.id = this.id + "_toolbar";
    		ul.className = "mbe_toolbar";
    		ul.style.width = this.width + "px";
    		this.toolbar = $(ul);
     
    		// Add Format Selection
    		var li = ul.appendChild(document.createElement("li"));
    		var select = li.appendChild(document.createElement("select"));
    		select.className = "formatSelect";
    		select.id = this.id + "_format";
    		select.options.add(new Option('Format Selection', '', false));
    		this.formatOptions.each(function(format) {
    			select.options.add(new Option(format.value, format.key, false));
    		});
    		this.formatSelect = $(select);
    		Event.observe(select, 'change', makeHandler(self, 'formatSelection'));
     
    		// Add Editor Tools
    		this.tools.each(function(toolGroup) {
    			var ulli = ul.appendChild(document.createElement("li"));
    			var ulul = ulli.appendChild(document.createElement("ul"));
    			toolGroup.each(function(tool) {		
    				var lili = ulul.appendChild(document.createElement("li"));
    				var a = lili.appendChild(document.createElement("a"));
    				a.setAttribute("title", tool.value);
    				a.className = tool.key;
    				a.appendChild(document.createTextNode(tool.value));
    				Event.observe(a, 'click', makeCommandHandler(self, tool.key, ''));
    			});
    		});
     
    		// Add Custom Tools Selection
    		var li = ul.appendChild(document.createElement("li"));
    		var select = li.appendChild(document.createElement("select"));
    		select.className = "customToolSelect";
    		select.id = this.id + "_custom_tool";
    		select.options.add(new Option('Tools', '', false));
    		this.customTools.each(function(tool) {
    			select.options.add(new Option(tool.value, tool.key, false));
    		});
    		this.toolSelect = $(select);
    		Event.observe(select, 'change', makeHandler(self, 'toolSelection'));
     
    		// Add View Source Tools
    		var ul = document.createElement("ul");
    		elements.push(ul);
    		ul.id = this.id + "_source_toolbar";
    		ul.className = "mbe_toolbar";
    		ul.style.width = this.width + "px";
    		ul.style.display = "none";
    		this.srcToolbar = $(ul);
     
    		this.sourceTools.each(function(tool) {
    			var li = ul.appendChild(document.createElement("li"));
    			var a = li.appendChild(document.createElement("a"));
    			a.title = tool.value;
    			a.className = tool.key;
    			a.appendChild(document.createTextNode(tool.value));
    			Event.observe(a, 'click', makeHandler(self, tool.key));
    		});
     
    		// Create iframe of same dimensions as textarea
    		var iframe = document.createElement("iframe");
    		elements.push(iframe);
    		iframe.id = this.id + "_iframe";
    		iframe.className = "mbe_iframe";
    		iframe.name = this.id + "_iframe";
    		iframe.width = this.width + "px";
    		iframe.height = this.height + "px";
    		iframe.frameBorder = 0;
    		this.iframe = $(iframe);
     
    		// Add toolbar and iframe before textarea
    		elements.each(function(element) {
    			textarea.parentNode.insertBefore(element, textarea);
    		});
    		this.textarea.hide();
    		this.contentWindow = this.iframe.contentWindow.document;
    		this.contentDocument = this.iframe.contentDocument;
     
    		// write the content of textarea to the iframe, If textarea contains nothing then set to default
    		var content = (this.textarea.value == '') ? '<html><head></head><body></body></html>' : this.textarea.value;
    		this.contentWindow.open();
    		this.contentWindow.write(content);
    		this.contentWindow.close();
     
    		// enable the designMode	
    		try {
    			this.contentWindow.designMode =  "on" ;
    			this.contentDocument.designMode = "on";
    		}
    		catch(e) { /* alert(e); */ }
    		// Event.observe(this.contentWindow, 'keypress', makeHandler(self, 'editorKeyPress'));
     
    	};
     
    	// assign the value of the iframe to the textarea
    	this.updateTextarea = function() {
    		this.textarea.value = this.contentWindow.body.innerHTML;
    	};
     
    	// assign the value of the textarea to the iframe
    	this.updateIframe = function() {
    		this.contentWindow.body.innerHTML = this.textarea.value;
    	};
     
    	// View Source
    	this.toggleSource = function() {
    		// Hide visible editor and show hidden editor
    		(this.textarea.visible()) ? this.updateIframe() : this.updateTextarea() ;
    		this.textarea.toggle();
    		this.srcToolbar.toggle();
    		this.toolbar.toggle();
    		this.iframe.toggle();
    	};
     
    	this.formatSelection = function() {
    		this.editorCommand('formatblock', '<' + this.formatSelect.options[this.formatSelect.selectedIndex].value + '>');
    		this.formatSelect.selectedIndex = 0;
    	};
     
    	this.toolSelection = function() {
    		var key = this.toolSelect.options[this.toolSelect.selectedIndex].value;
    		switch(key) {
     
    			// View HTML Source
    			case 'toggleSource':
    				this.toggleSource();
    				break;
    			case '': break;
    			default:
    				alert('Sorry, The Tool "' + key + '" has not been implemented yet :(');
    				break;
    		}
    		this.toolSelect.selectedIndex = 0;
    	};
     
    	this.sourceToolSelection = function(key) {
    		switch(key) {
     
    			// View HTML Source
    			case 'toggleSource':
    				this.toggleSource();
    				break;
    			case '': break;
    			default:
    				alert('Sorry, The Source Tool "' + key + '" has not been implemented yet :(');
    				break;
    		}
    	};
     
    	this.addContent = function(content) {
    		(this.textarea.visible()) ?	this.textarea.value += content : this.contentWindow.write(content);
    	};
     
    	this.editorCommand = function(command, options) {
    		var rte = this.iframe.contentWindow;
    		try {
    			rte.focus();
    			rte.document.execCommand(command, false, options);
    			rte.focus();
    		} catch (e) { alert(e); }
    	};
     
    	this.editorKeyPress = function(event) {
    		var key = event.which || event.keyCode;
    		// Shift + key events
    		if (event.shiftKey) {
    			switch (key) {
    				case Event.KEY_RETURN:
    					// Do line break
    					this.addContent('<br />');
    					Event.stop(event);
    					break;
    			}
    		}
    		else {
    			switch (key) {
    				case Event.KEY_RETURN:
    					// Do Paragraph
    					this.editorCommand('formatblock', '<P>');
    					Event.stop(event);
    					break;
    			}
    		}
    	};
    };
     
    // init method
    Event.observe(window, 'load', function() {
     
    	// textarea will only be added if browser supports designMode
    	if (document.designMode) {
     
    		// Get all textarea's in page with class="rte"
    		rtes = $$('textarea.mbe');
    		rtes.each(function(textarea) {
    			// Replace textarea with editable iframe
    			mbe = new MBEditor();
    			mbe.init(textarea);
    		});
    	}
    });
    Writing cross-browser javascript is very frustrating. Doing it 'the right way' is also frustrating because 'the bad old way' seems to be more stable across the board.

  13. #13
    padawan silver trophybronze trophy markbrown4's Avatar
    Join Date
    Jul 2006
    Location
    Victoria, Australia
    Posts
    4,122
    Mentioned
    29 Post(s)
    Tagged
    2 Thread(s)
    Here's a solution that solves the above problems.. it's working but getting uglier by the second.
    Code javascript:
    /* This class initialises on load event of window
    * It replaces all textareas with a class of 'mbe' with a rich text editor
    **/
    var MBEditor = function() {
     
    	// Formatting Selection
    	this.formatOptions = $H({
    		P: 'Normal',
    		H1: 'Heading 1',
    		H2: 'Heading 2',
    		H3: 'Heading 3',
    		H4: 'Heading 4',
    		H5: 'Heading 5',
    		H6: 'Heading 6'
    	});
     
    	// Create an array of hash groups with the default tools that use execCommand
    	this.tools = [];
    	this.tools.push($H({
    		bold: 'Bold',
    		italic: 'Italic'
    	}));
    	this.tools.push($H({
    		// underline: 'Underline',
    		justifyleft: 'Align Left',
    		justifycenter: 'Align Center',
    		justifyright: 'Align Right'
    	}));
    	this.tools.push($H({
    		insertorderedlist: 'Numbered List',
    		insertunorderedlist: 'Bulletpoint List',
    		outdent: 'Outdent',
    		indent: 'Indent'
    	}));
    	this.tools.push($H({
    		undo: 'Undo',
    		redo: 'Redo'
    	}));
     
    	// Custom Tools
    	this.customTools = $H({
    		image: 'Insert Image',
    		table: 'Insert Table',
    		link: 'Insert Link',
    		anchor: 'Insert Anchor',
    		manageAnchors: 'Manage Anchors',
    		manageFiles: 'Manage Files',
    		getSelection: 'Test Range',
    		toggleSource: 'Edit HTML Source'
    	});
     
    	// Tools for HTML editing
    	this.sourceTools = $H({
    		toggleSource: 'View Editor'
    	});
     
    	this.init = function(textarea) {
     
    		this.textarea = textarea;
    		this.id = textarea.getAttribute('id');
    		var dim = textarea.getDimensions();
    		this.height = dim.height;
    		this.width = dim.width;
     
    		// Reference to this object for event registration
    		var self = this;
     
    		// Closures for Event Registration
    		var makeHandler = function(subject, method) {
    			return function() { subject[method](); }
    		};
     
    		var makeCommandHandler = function(subject, cmd, opt) {
    			return function() { subject['editorCommand'](cmd, opt); }
    		};
     
    		// Array to hold our elements to add into the document
    		var elements = [];
     
    		// Create Toolbar
    		var ul = document.createElement("ul");
    		elements.push(ul);
    		ul.id = this.id + "_toolbar";
    		ul.className = "mbe_toolbar";
    		ul.style.width = this.width + "px";
    		this.toolbar = $(ul);
     
    		// Add Format Selection
    		var li = ul.appendChild(document.createElement("li"));
    		var select = li.appendChild(document.createElement("select"));
    		select.className = "formatSelect";
    		select.id = this.id + "_format";
    		select.options.add(new Option('Format Selection', '', false));
    		this.formatOptions.each(function(format) {
    			select.options.add(new Option(format.value, format.key, false));
    		});
    		this.formatSelect = $(select);
    		Event.observe(select, 'change', makeHandler(self, 'formatSelection'));
     
    		// Add Editor Tools
    		this.tools.each(function(toolGroup) {
    			var ulli = ul.appendChild(document.createElement("li"));
    			var ulul = ulli.appendChild(document.createElement("ul"));
    			toolGroup.each(function(tool) {		
    				var lili = ulul.appendChild(document.createElement("li"));
    				var a = lili.appendChild(document.createElement("a"));
    				a.setAttribute("title", tool.value);
    				a.className = tool.key;
    				a.appendChild(document.createTextNode(tool.value));
    				a.href= 'javascript:editorCommand("'+self.id+'", "'+tool.key+'")';
    				//Event.observe(a, 'click', makeCommandHandler(self, tool.key, ''));
    			});
    		});
     
    		// Add Custom Tools Selection
    		var li = ul.appendChild(document.createElement("li"));
    		var select = li.appendChild(document.createElement("select"));
    		select.className = "customToolSelect";
    		select.id = this.id + "_custom_tool";
    		select.options.add(new Option('Tools', '', false));
    		this.customTools.each(function(tool) {
    			select.options.add(new Option(tool.value, tool.key, false));
    		});
    		this.toolSelect = $(select);
    		Event.observe(select, 'change', makeHandler(self, 'toolSelection'));
     
    		// Add View Source Tools
    		var ul = document.createElement("ul");
    		elements.push(ul);
    		ul.id = this.id + "_source_toolbar";
    		ul.className = "mbe_toolbar";
    		ul.style.width = this.width + "px";
    		ul.style.display = "none";
    		this.srcToolbar = $(ul);
     
    		this.sourceTools.each(function(tool) {
    			var li = ul.appendChild(document.createElement("li"));
    			var a = li.appendChild(document.createElement("a"));
    			a.title = tool.value;
    			a.className = tool.key;
    			a.appendChild(document.createTextNode(tool.value));
    			Event.observe(a, 'click', makeHandler(self, tool.key));
    		});
     
    		// Create iframe of same dimensions as textarea
    		var iframe = document.createElement("iframe");
    		elements.push(iframe);
    		iframe.id = this.id + "_iframe";
    		iframe.name = this.id + "_iframe";
    		iframe.className = "mbe_iframe";
    		iframe.name = this.id + "_iframe";
    		iframe.width = this.width + "px";
    		iframe.height = this.height + "px";
    		iframe.frameBorder = 0;
    		this.iframe = $(iframe);
     
    		// Add toolbar and iframe before textarea
    		elements.each(function(element) {
    			textarea.parentNode.insertBefore(element, textarea);
    		});
    		this.textarea.hide();
    		this.contentWindow = this.iframe.contentWindow;
     
    		// write the content of textarea to the iframe, If textarea contains nothing then set to default
    		var content = (this.textarea.value == '') ? '<html><head></head><body></body></html>' : this.textarea.value;
    		this.contentWindow.document.open();
    		this.contentWindow.document.write(content);
    		this.contentWindow.document.close();
     
    		// enable the designMode	
    		try {
    			this.contentWindow.document.designMode =  "on";
    			this.contentWindow.designMode = "on";
    		}
    		catch(e) { /* alert(e); */ }
    	};
     
    	// assign the value of the iframe to the textarea
    	this.updateTextarea = function() {
    		this.textarea.value = this.contentWindow.document.body.innerHTML;
    	};
     
    	// assign the value of the textarea to the iframe
    	this.updateIframe = function() {
    		this.contentWindow.document.body.innerHTML = this.textarea.value;
    	};
     
    	// View Source
    	this.toggleSource = function() {
    		// Hide visible editor and show hidden editor
    		(this.textarea.visible()) ? this.updateIframe() : this.updateTextarea() ;
    		this.textarea.toggle();
    		this.srcToolbar.toggle();
    		this.toolbar.toggle();
    		this.iframe.toggle();
    	};
     
    	this.formatSelection = function() {
    		this.editorCommand('formatblock', '<' + this.formatSelect.options[this.formatSelect.selectedIndex].value + '>');
    		this.formatSelect.selectedIndex = 0;
    	};
     
    	this.toolSelection = function() {
    		var key = this.toolSelect.options[this.toolSelect.selectedIndex].value;
    		switch(key) {
     
    			// View HTML Source
    			case 'toggleSource':
    				this.toggleSource();
    				break;
    			case 'image':
    				this.launchDialog('insertImage.html', 400, 200);
    				break;
    			case 'getSelection':
    				alert(this.getSelectedElement());
    				break;
    			case '': break;
    			default:
    				alert('Sorry, The Tool "' + key + '" has not been implemented yet :(');
    				break;
    		}
    		this.toolSelect.selectedIndex = 0;
    	};
     
    	this.sourceToolSelection = function(key) {
    		switch(key) {
     
    			// View HTML Source
    			case 'toggleSource':
    				this.toggleSource();
    				break;
    			case '': break;
    			default:
    				alert('Sorry, The Source Tool "' + key + '" has not been implemented yet :(');
    				break;
    		}
    	};
     
    	this.addContent = function(content) {
    		(this.textarea.visible()) ?	this.textarea.value += content : this.contentDocument.write(content);
    	};
     
    	this.editorCommand = function(command, options) {
    		try {
    			this.contentWindow.focus();
    			this.contentWindow.document.execCommand(command, false, options);
    			this.contentWindow.focus();
    		} catch (e) { alert(e); }
    	};
     
    	this.editorKeyPress = function(event) {
    		var key = event.which || event.keyCode;
    		// Shift + key events
    		if (event.shiftKey) {
    			switch (key) {
    				case Event.KEY_RETURN:
    					// Do line break
    					this.addContent('<br />');
    					Event.stop(event);
    					break;
    			}
    		}
    		else {
    			switch (key) {
    				case Event.KEY_RETURN:
    					// Do Paragraph
    					this.editorCommand('formatblock', '<P>');
    					Event.stop(event);
    					break;
    			}
    		}
    	};
     
    	this.getSelectedElement = function() {
    		var s = this.contentWindow.getSelection();
    		return s.anchorNode.parentNode;
    	};
     
    	this.launchDialog = function(url, width, height) {
    		window.open(url, '', 'width='+width+',height='+height);
    	}
    };
     
    // init method
    Event.observe(window, 'load', function() {
     
    	// textarea will only be added if browser supports designMode
    	if (document.designMode) {
     
    		// Get all textarea's in page with class="rte"
    		rtes = $$('textarea.mbe');
    		rtes.each(function(textarea) {
    			// Replace textarea with editable iframe
    			mbe = new MBEditor();
    			mbe.init(textarea);
    		});
    	}
    });
     
    function editorCommand(editor, command, option) {
     
    	var rte = $(editor + '_iframe').contentWindow;
    	try {
    		rte.focus();
    		rte.document.execCommand(command, false, option);
    		rte.focus();
    	} catch (e) { }
    }
    Last edited by markbrown4; Dec 27, 2007 at 21:59.

  14. #14
    padawan silver trophybronze trophy markbrown4's Avatar
    Join Date
    Jul 2006
    Location
    Victoria, Australia
    Posts
    4,122
    Mentioned
    29 Post(s)
    Tagged
    2 Thread(s)
    Hi,

    I've updated the editor now, i'm just interested in hearing any reccomendations for improvement of the scripts. I'm still just a beginner with js, thoroughly enjoyed Simply Javascript and Bulletproof Ajax - and it seems like I will be doing a lot more js in my new job.

    linky linky

    I've tried to do it 'The right way' first, separation of concerns, DOM and the modern standard event model - and then when some browser didn't have support for some feature I would provide an alternative solution. I guess i'm just looking for some feedback on my style of js code and if it can be improved.

    For example - in other editors I have seen people choosing to extend the methods of the textarea itself and giving it properties and methods. I'm completely unfamiliar with this style of javascript and using the prototype keyword.

    The JS:
    Code javascript:
    /*
     * This class replaces all textareas with a class of 'mbe' with a rich text editor
     *
     * There are 2 elements that hold the content of the editor, the textarea which holds the source
     * and the iframe which hold a 'document' representation of that source.
     * On Load, Submit and 'View Source' the content is copied into the non-visible element.
     * Version 0.3
     * Author: Mark brown
    **/
     
    var mbe = [];
     
    var MBEditor = function() {
     
    	// This array holds the stylesheets for iframe
    	this.stylesheets = ['css/iframe.css'];
     
    	// Formatting Selection
    	this.formatOptions = $H({
    		P: 'Normal',
    		H1: 'Heading 1',
    		H2: 'Heading 2',
    		H3: 'Heading 3',
    		H4: 'Heading 4',
    		H5: 'Heading 5',
    		H6: 'Heading 6'
    	});
     
    	// Create an array of hash groups with the default tools that use execCommand
    	this.tools = [];
    	this.tools.push($H({
    		bold: 'Bold',
    		italic: 'Italic'
    	}));
    	this.tools.push($H({
    		// underline: 'Underline',
    		justifyleft: 'Align Left',
    		justifycenter: 'Align Center',
    		justifyright: 'Align Right'
    	}));
    	this.tools.push($H({
    		insertorderedlist: 'Numbered List',
    		insertunorderedlist: 'Bulletpoint List',
    		outdent: 'Outdent',
    		indent: 'Indent'
    	}));
    	this.tools.push($H({
    		undo: 'Undo',
    		redo: 'Redo'
    	}));
     
    	// Custom Tools
    	this.customTools = $H({
    		image: 'Insert Image',
    		document: 'Insert Document',
    		table: 'Insert Table',
    		link: 'Insert Link',
    		anchor: 'Insert Anchor',
    		// manageFiles: 'Manage Files',
    		toggleSource: 'Edit HTML Source'
    	});
     
    	// Tools for HTML editing
    	this.sourceTools = $H({
    		toggleSource: 'View Editor'
    	});
     
    	this.init = function(i, textarea) {
     
    		this.i = i;
    		this.textarea = textarea;
    		this.id = textarea.getAttribute('id');
    		var dim = textarea.getDimensions();
    		this.height = dim.height;
    		this.width = dim.width;
     
    		// Get form that is holding the textarea
    		this.form = textarea.up('form');
     
    		// When the form holding the editor is submitted we need to update the source in the textarea
    		Event.observe(this.form, 'submit', this.updateTextarea.bindAsEventListener(this));
     
    		// You lose a reference to 'this' within Prototype's Enumerators - items.each(function(item){ /* in here */ });
    		var self = this;
     
    		// Array to hold our elements to add into the document
    		var elements = [];
     
    		var makeHandler = function(subject, method) {
    			return function() { subject[method](); }
    		}
     
    		// Create Toolbar
    		var ul = document.createElement("ul");
    		elements.push(ul);
    		ul.id = this.id + "_toolbar";
    		ul.className = "mbe_toolbar";
    		ul.style.width = this.width + "px";
    		this.toolbar = $(ul);
     
    		// Add Format Selection
    		var li = ul.appendChild(document.createElement("li"));
    		var select = li.appendChild(document.createElement("select"));
    		select.className = "formatSelect";
    		select.id = this.id + "_format";
    		select.options.add(new Option('Format Selection', '', false));
    		this.formatOptions.each(function(format) {
    			select.options.add(new Option(format.value, format.key, false));
    		});
    		this.formatSelect = $(select);
    		Event.observe(select, 'change', makeHandler(self, 'formatSelection'));
     
    		// Add Editor Tools
    		this.tools.each(function(toolGroup) {
    			var ulli = ul.appendChild(document.createElement("li"));
    			var ulul = ulli.appendChild(document.createElement("ul"));
    			toolGroup.each(function(tool) {		
    				var lili = ulul.appendChild(document.createElement("li"));
    				var a = lili.appendChild(document.createElement("a"));
    				a.setAttribute("title", tool.value);
    				a.className = tool.key;
    				a.appendChild(document.createTextNode(tool.value));
    				a.href= 'javascript:mbe['+i+'].editorCommand("'+tool.key+'", "")';
    			});
    		});
     
    		// Add Custom Tools Selection
    		var li = ul.appendChild(document.createElement("li"));
    		var select = li.appendChild(document.createElement("select"));
    		select.className = "customToolSelect";
    		select.id = this.id + "_custom_tool";
    		select.options.add(new Option('Tools', '', false));
    		this.customTools.each(function(tool) {
    			select.options.add(new Option(tool.value, tool.key, false));
    		});
    		this.toolSelect = $(select);
    		Event.observe(select, 'change', makeHandler(self, 'toolSelection'));
     
    		// Add View Source Tools
    		var ul = document.createElement("ul");
    		elements.push(ul);
    		ul.id = this.id + "_source_toolbar";
    		ul.className = "mbe_toolbar";
    		ul.style.width = this.width + "px";
    		ul.style.display = "none";
    		this.srcToolbar = $(ul);
     
    		this.sourceTools.each(function(tool) {
    			var li = ul.appendChild(document.createElement("li"));
    			var a = li.appendChild(document.createElement("a"));
    			a.title = tool.value;
    			a.className = tool.key;
    			a.appendChild(document.createTextNode(tool.value));
    			Event.observe(a, 'click', makeHandler(self, tool.key));
    		});
     
    		// Create iframe of same dimensions as textarea
    		var iframe = document.createElement("iframe");
    		elements.push(iframe);
    		iframe.id = this.id + "_iframe";
    		iframe.name = this.id + "_iframe";
    		iframe.className = "mbe_iframe";
    		iframe.name = this.id + "_iframe";
    		iframe.width = this.width + "px";
    		iframe.height = this.height + "px";
    		iframe.frameBorder = 0;
    		this.iframe = $(iframe);
     
    		// Add toolbar and iframe before textarea
    		elements.each(function(element) {
    			textarea.parentNode.insertBefore(element, textarea);
    		});
     
    		this.textarea.hide();
    		this.contentWindow = this.iframe.contentWindow;
    		Event.observe(this.contentWindow, 'keyup', this.editorKeyUpHandler.bindAsEventListener(this));
     
    		// write the content of textarea to the iframe, an embedded stylesheet was all that worked in multiple browsers..
    		this.contentWindow.document.open();
    		this.contentWindow.document.write('<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"><html><head>');
     
    		// For Prototypes iterator loss of 'this' reference
    		var cw = this.contentWindow;
     
    		// IE only works with embedded styles
    		if (Prototype.Browser.IE) {
    			this.contentWindow.document.write('<style type="text/css">');
    			this.stylesheets.each(function(ss) {
    				cw.document.write('@import url('+ss+');');
    			});
     
    			cw.document.write('</style>');
    		}
    		cw.document.write('</head><body class="editor_body">'+this.textarea.value+'</body></html>');
    		cw.document.close();
     
    		if (!Prototype.Browser.IE) {
    			// Attach Editors stylsheets the DOM way
    			this.stylesheets.each(function(ssLink) {
    				var ss = document.createElement('link');
    				ss.href = ssLink;
    				ss.type = 'text/css';
    				ss.rel = 'stylesheet';
    				ss.media = 'screen';
     
    				cw.document.getElementsByTagName('head')[0].appendChild(ss);
    			});
    		}
    		// enable the designMode	
    		try {
    			this.contentWindow.document.designMode =  "on";
    			this.contentWindow.designMode = "on";
    		}
    		catch(e) { /* alert(e); */ }
     
    		// FF's 'styledWithCSS' html is bloated
    		if (Prototype.Browser.Gecko)
    			this.editorCommand('styleWithCSS', false);
    	};
     
    	// assign the value of the iframe to the textarea
    	this.updateTextarea = function() {
    		this.textarea.value = this.contentWindow.document.body.innerHTML;
    	};
     
    	// assign the value of the textarea to the iframe
    	this.updateIframe = function() {
    		this.contentWindow.document.body.innerHTML = this.textarea.value;
    	};
     
    	// View Source
    	this.toggleSource = function() {
    		// Hide visible editor and show hidden editor
    		(this.textarea.visible()) ? this.updateIframe() : this.updateTextarea() ;
    		this.textarea.toggle();
    		this.srcToolbar.toggle();
    		this.toolbar.toggle();
    		this.iframe.toggle();
    	};
     
    	/* A format from this.formatOptions has been selected */
    	this.formatSelection = function() {
    		this.editorCommand('formatblock', '<' + this.formatSelect.options[this.formatSelect.selectedIndex].value + '>');
    		this.formatSelect.selectedIndex = 0;
    	};
     
    	/* A tool from this.customTools has been selected */
    	this.toolSelection = function() {
    		var key = this.toolSelect.options[this.toolSelect.selectedIndex].value;
    		switch(key) {
     
    			case 'toggleSource':
    				this.toggleSource();
    				break;
    			case 'image':
    				this.launchDialog('insertImage.html', 400, 150);
    				break;
    			case 'link':
    				this.launchDialog('insertLink.html', 400, 150);
    				break;
    			case 'table':
    				this.launchDialog('insertTable.html', 400, 250);
    				break;
    			case 'anchor':
    				this.launchDialog('insertAnchor.html', 400, 100);
    				break;
    			case 'document':
    				this.launchDialog('insertDocument.html', 400, 150);
    				break;
    			case '': break;
    			default:
    				alert('Sorry, The Tool "' + key + '" has not been implemented yet :(');
    				break;
    		}
    		this.toolSelect.selectedIndex = 0;
    	};
     
    	// Tools from the view source menu
    	this.sourceToolSelection = function(key) {
    		switch(key) {
     
    			// View HTML Source
    			case 'toggleSource':
    				this.toggleSource();
    				break;
    			case '': break;
    			default:
    				alert('Sorry, The Source Tool "' + key + '" has not been implemented yet :(');
    				break;
    		}
    	};
     
    	// Uses insertHTML execCommand to put HTML into the editor at the point of selection
    	this.addContent = function(content) {
     
    		// IE doesn't have an insertHTML command
    		if (Prototype.Browser.IE) {
    			var elm = this.getSelectedContainer();
    			if (this.contentWindow.document.body.innerHTML == '') {
    				// Empty textarea - Just set the contents of the body
    				this.contentWindow.document.body.innerHTML = content;
    			}
    			else {
    				// Add content after the selected element
    				new Insertion.After(elm, content);
    			}
    		}
    		else {
    			this.editorCommand('insertHTML', content);
    		}
    	};
     
    	// insert an HTML element *after* the current one
    	this.insertAfter = function(elem, currentElem) {
    		if (currentElem.nextSibling != null)
    			currentElem.parentNode.insertBefore(elem,currentElem.nextSibling);
    		else if (currentElem.nodeName.toLowerCase() == 'body')
    			currentElem.appendChild(elem);
    		else if (currentElem.parentElement)
    		    currentElem.parentElement.appendChild(elem);
    		else
    			currentElem.parentNode.appendChild(elem);
    	};
     
       /** Calls execCommand function - various levels of support in browsers and different html output
    	 * Helpful links regarding designMode and execCommand
    	 * [url]http://msdn2.microsoft.com/en-us/library/ms533049(VS.85).aspx[/url]
    	 * [url]http://www.mozilla.org/editor/ie2midas.html[/url] 
    	**/
    	this.editorCommand = function(command, options) {
    		try {
    			this.contentWindow.focus();
    			this.contentWindow.document.execCommand(command, false, options);
    			this.contentWindow.focus();
    		} catch (e) { alert(e); }
    	};
     
    	/* Range and Selection objects are created and used to get information about the currently 
    	 * selected content in a document, IE uses a different implementation to other browsers.
    	**/
    	this.getSelection = function() {
    		return (window.getSelection) ? this.contentWindow.getSelection() : this.contentWindow.document.selection.createRange();
    	};
     
    	/* Returns the block that is in the currect Selection */
    	this.getSelectedContainer = function() {
     
    		var s = this.getSelection();
    		if (s.parentElement && s.parentElement != undefined) {
    			elm = s.parentElement();
    			/* IE loses selection focus when you click outside the editor so ensure the selection 
    			 * is within a text editor. A bit of a hack to see where the selection is */
    			try {
    				var test = elm.up('');
    			}
    			catch(e) {return elm;}
    			return false;
    		}
    		else {
    			var node = s.focusNode;
    			if (node.nodeName == "#text") {
    				return node.parentNode;
    			}
    			else {
    				return node;
    			}
    		}
    		return false;
    	}
    	this.launchDialog = function(url, width, height) {
    		if (this.getSelectedContainer() == false)
    			alert('Please click the editor at the point of insertion before selecting tool.');
    		else
    			window.open(url+'?id=' + this.i, '', 'width='+width+',height='+height);
    	};
     
    	this.updateFromPopup = function(formData) {
    		// alert(formData.asString);
    		switch(formData.asHash.tool) {
    			case 'image':
    				this.addContent('<img src="'+formData.asHash.src+'" alt="'+formData.asHash.alt+'" />');
    				break;
    			case 'link':
    				this.addContent('<a href="'+formData.asHash.href+'">'+formData.asHash.text+'</a>');
    				break;
    			case 'table':
    				var rows = parseInt(formData.asHash.rows);
    				var cols = parseInt(formData.asHash.cols);
    				var border = (formData.asHash.border == 'checked');
    				var headings = (formData.asHash.headings == 'checked')
    				var table = this.getTable(rows, cols, border, headings);
    				this.addContent(table);
    				break;
    			case 'anchor':
    				var a = this.contentWindow.document.createElement('a');
    				a.className = 'namedAnchor';
    				a.name = formData.asHash.name;
    				a.title = formData.asHash.name;
    				a.alt = formData.asHash.name;
    				// Insert after selection
    				this.insertAfter(a, this.getSelectedContainer());
    				// Setup event on click to remove anchor
    				var anchorHandler = function(event) { a.parentNode.removeChild(a); }
    				Event.observe(a, 'click', anchorHandler);
    				break;
    			case 'document':
    				this.addContent('<img src="images/file.png" alt="Download File" />&nbsp;<a href="'+formData.asHash.href+'">'+formData.asHash.text+'</a>');
    				break;
    			default:
    				alert('Sorry, The popup Tool "' + formData.asHash.tool + '" has not been implemented yet :(');
    				break;
    		}
    	};
     
    	// Completely remove the element that fires the event
    	this.removeEventFiringElement = function(event) {
    		var elm = Event.element(event);
    		alert(elm);
    	};
     
    	// Generate table markup
    	this.getTable = function(rows, cols, border, headings) {
    		var table = '<table';
    		if (border)
    			table += ' class="border" border="1"';
    		table += '>';
    		if (headings) {
    			table += "<thead><tr>";
    			for (var i=0; i<cols; i++) {
    				table += "<th></th>";
    			}
    			table += "</tr></thead>";
    		}
    		table += "<tbody>";
    		for (var i=0; i<rows; i++) {
    			table += "<tr>";
    			for (var j=0; j<cols; j++) {
    				table += "<td></td>";
    			}
    			table += "</tr>";
    		}
    		table += "</tbody>";
    		table += "</table>";
     
    		return table;
    	};
     
    	// Cleans up generated markup - makes ff closer to IE's rendering
    	this.editorKeyUpHandler = function(e) {
     
    		var key = e.which || e.keyCode;
    		var elm = this.getSelectedContainer();
    		var name = elm.tagName.toLowerCase();
    		var shift = e.shiftKey;
     
    		switch (key) {
    			case Event.KEY_RETURN:
    				if (!shift) {
    					// Cleanup <br><br>'s in ff
    					var nodes = this.contentWindow.document.body.childNodes;
    					for (var i=0; i<nodes.length; i++) {
    						if (nodes.item(i).nodeName.toLowerCase() == 'br') {
    							this.contentWindow.document.body.removeChild(nodes.item(i));
    						}
    					}
    				}
    				var inlineTags = ['strong', 'b', 'em', 'i', 'sub', 'sup', 'a'];
    				if (inlineTags.indexOf(name) != -1)
    					name = elm.parentNode.tagName.toLowerCase();
    				if (name == 'body')
    					this.editorCommand('formatblock', '<P>');
    				break;
    			default:
    				if (name == 'body') {
    					// make stray elements paragraphs
    					this.editorCommand('formatblock', '<P>');
    				}
    				break;
    		}
    	};
    };
     
    // init method
    Event.observe(window, 'load', function() {
     
    	// textarea will only be added if browser supports designMode
    	if (document.designMode) {
     
    		// Get all textarea's in page with class="rte"
    		rtes = $$('textarea.mbe');
    		var i = 0;
    		rtes.each(function(textarea) {
    			// Replace textarea with editable iframe
    			mbe[i] = new MBEditor();
    			mbe[i].init(i, textarea);
    			i++;
    		});
    	}
    });

    Working with Rich Text editors is an imperfect science as your at the mercy of the browser manufacturors, and because it's not a standard - each browser implements it in the way that they think best. I actually prefer the way IE works by default to other browsers.

    If I were tackle this again, I would use a completely different model for IE and use the contentEditable attribute so that I wasn't working with the iframe bugs. Selection and Range objects and references to iframe's 'document' is very buggy in IE.

    Thanks for your help peoples,

  15. #15
    SitePoint Enthusiast Mr Moo's Avatar
    Join Date
    Oct 2007
    Posts
    30
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    First, might I suggest you switch to the latest version of Prototype. 1.6 has been out for a while already. Second, since you're using Prototype, why not use the x-browser event binding that it provides? Using it is very simple and allows you to bind your event handlers to your class, and create elements with great ease:
    Code JavaScript:
    MBEditor = Class.create({
      initialize: function(element) {
        this.element = $(element);
        //Do other setup stuff
        this.toolbar = new Element("div", { className: "toolbar" });
        this.body.insert(this.toolbar);
        this.bold = new Element("button", { className: "button" }).update("B").observe("click", this.boldClicked.bind(this));
        this.toolbar.insert(this.bold);
      },
      boldClicked: function(event) {
        alert(this.element.identify() + " bold clicked");
      }
    });
    Not one shred of evidence supports the notion that life is serious.
    eternal.co.za - code, thoughts, rants and raves
    f1rivals.net - formula 1 forums, and, hopefully, soon, prediction game

  16. #16
    padawan silver trophybronze trophy markbrown4's Avatar
    Join Date
    Jul 2006
    Location
    Victoria, Australia
    Posts
    4,122
    Mentioned
    29 Post(s)
    Tagged
    2 Thread(s)
    Excellent,

    Thanks Mr Moo, i'll definitely go through and update my script with your suggestions.


Bookmarks

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •