Friday, May 23, 2008

Dojo Widget for in-browser editor CodeMirror

CodeMirror, a very impressive in-browser code editor for Javascript, XML/HTML or CSS (or any language, you just have to plug in a your own parser) made some nice progress in the last months. CodeMirror has no dependency on other frameworks or libraries. If you want to use it in a dojo environment as a dojo compatible widget, here I am gonna share here a little tutorial how to write such a thing:

First download CodeMirror and transform CodeMirror.js (the main file which loads the other files into an iframe), into something like this:

dojo.provide("mystuff.widget.CodeMirror");
dojo.require("dijit._Widget");


dojo.declare("mystuff.widget.CodeMirror", dijit._Widget, {

initialized: false,

// currently supported: 'xml' (HTML), 'js' or 'css'
type: 'xml',

options: {
stylesheet: "",
path: "/static/codemirror/js/",
parserfiles: [],
basefiles: ["codemirror_iframe.js"],
linesPerPass: 15,
passDelay: 200,
continuousScanning: false,
saveFunction: function() {
console.log('save');
},
content: " ",
undoDepth: 20,
undoDelay: 800,
disableSpellcheck: true,
textWrapping: true,
readOnly: false,
width: "100%",
height: "100%",
parserConfig: null
},

postMixInProperties: function() {
this.options.stylesheet = "/static/codemirror/css/" + this.type + "colors.css";
this.options.parserfiles = ["parse" + this.type + ".js"];
},

postCreate: function() {
this.inherited(arguments);
},

startup: function() {
if (dijit._isElementShown(this.domNode.parentNode))
this.initialize();
},

initialize: function() {
if (this.initialized)
return;

frame = document.createElement("IFRAME");
frame.style.border = "0";
frame.style.width = this.options.width;
frame.style.height = this.options.height;
// display: block occasionally suppresses some Firefox bugs, so we
// always add it, redundant as it sounds.
frame.style.display = "block";

this.domNode.appendChild(frame);

// Link back to this object, so that the editor can fetch options
// and add a reference to itself.
frame.CodeMirror = this;
this.win = frame.contentWindow;

var _this = this;
var html = ["<html><head><link rel=\"stylesheet\" type=\"text/css\" href=\"" + this.options.stylesheet + "\"/>"];
dojo.forEach(this.options.basefiles.concat(this.options.parserfiles), function(file) {
html.push("<script type=\"text/javascript\" src=\"" + _this.options.path + file + "\"></script>");
});
html.push("</head><body style=\"border-width: 0;\" class=\"editbox\" spellcheck=\"" +
(this.options.disableSpellcheck ? "false" : "true") + "\"></body></html>");

var doc = this.win.document;
doc.open();
doc.write(html.join(""));
doc.close();

this.initialized = true;
},

getCode: function() {
return this.editor.getCode();
},

setCode: function(code) {
this.editor.importCode(code);
},

focus: function() {
this.win.focus();
},

jumpToChar: function(start, end) {
this.editor.jumpToChar(start, end);
this.focus();
},

jumpToLine: function(line) {
this.editor.jumpToLine(line);
this.focus();
},

currentLine: function() {
return this.editor.currentLine();
},

selection: function() {
return this.editor.selectedText();
},

reindent: function() {
this.editor.reindent();
},

replaceSelection: function(text, focus) {
this.editor.replaceSelection(text);
if (focus) this.focus();
},

replaceChars: function(text, start, end) {
this.editor.replaceChars(text, start, end);
},

getSearchCursor: function(string, fromCursor) {
return this.editor.getSearchCursor(string, fromCursor);
}
});
Then your should concatenate and minifiy (I use YUI compressor) all the JS files:

util.js, stringstream.js, select.js, undo.js, editor.js
=> codemirror_iframe.js

Also minify the parsers: parsejavascript.js, tokenizejavascript.js, parsecss.js, parsexml.js

At this point you can embed the widget into a test page:
<div
id="html_editor"
dojoType="mystuff.widget.CodeMirror"
type="xml"
style="height:100%;">
</div>
Now comes the tricky part: the editor does not initialize automatically at page load, because with dojo, chances are very high, you are gonna use this widget inside a container, where the editor is hidden at page load, and that would cause trouble with some browsers. So you need to subscribe to an event which triggers the visibility of the editor, so it can be lazy-initialized at first time it becomes visible. For example to use CodeMirror inside a tab container with a tab with id 'html_editor_tab' I do something like this:
dojo.declare("MyApp", null, {
main_tab_selected: function(page) {
if (page.id == 'html_editor_tab'){
dijit.byId('html_editor').initialize();
}
}
});

var myAppInstance = new MyApp();

dojo.subscribe("main_tabs-selectChild", myAppInstance, "main_tab_selected");
Update: Uhh, where was my brain when I wrote this article, I accidentally "misspelled" CodeMirror with CodePress at several places, including the title, it's corrected now ...

2 comments:

Marijn said...

s/CodePress/CodeMirror/g

Roberto Saccon said...

Thanks. It's corrected now.