diff --git a/DumbRenderer.js b/DumbRenderer.js
new file mode 100644
index 00000000..119637ff
--- /dev/null
+++ b/DumbRenderer.js
@@ -0,0 +1,189 @@
+function DumbRenderer(containerId)
+{
+ this.container = document.getElementById(containerId);
+ this.canvas = document.createElement("div");
+ this.canvas.className = "canvas";
+ this.container.appendChild(this.canvas);
+
+ this._measureSizes();
+
+ this.composition = document.createElement("div");
+ this.composition.className = "composition";
+ this.composition.style.height = this.lineHeight + "px";
+
+ this.cursor = document.createElement("div");
+ this.cursor.className = "cursor";
+ this.cursor.style.height = this.lineHeight + "px";
+}
+
+DumbRenderer.prototype =
+{
+ setDocument : function(doc) {
+ this.lines = doc.lines;
+ this.doc = doc;
+ },
+
+ getContainerElement : function() {
+ return this.container;
+ },
+
+ _measureSizes : function()
+ {
+ var measureNode = document.createElement("div");
+ var style = measureNode.style;
+ style.width = style.height = "auto";
+ style.left = style.top = "-1000px";
+ style.visibility = "hidden";
+ style.position = "absolute";
+ style.overflow = "visible";
+
+ measureNode.innerHTML = "X
X";
+ this.canvas.appendChild(measureNode);
+
+ this.lineHeight = Math.round(measureNode.offsetHeight / 2);
+ this.characterWidth = measureNode.offsetWidth;
+
+ this.canvas.removeChild(measureNode);
+ },
+
+ getLongestLineWidth : function(lines)
+ {
+ var longestLine = this.container.clientWidth;
+ for (var i=0; i < lines.length; i++) {
+ longestLine = Math.max(longestLine, (lines[i].length * this.characterWidth));
+ }
+ return longestLine;
+ },
+
+ draw : function()
+ {
+ var lines = this.lines;
+ var longestLine = this.getLongestLineWidth(lines);
+
+ var html = [];
+ for (var i=0; i < lines.length; i++)
+ {
+ html.push(
+ "
",
+ lines[i].
+ replace(/&/g, "&").
+ replace(/"
+ );
+ };
+ this.canvas.innerHTML = html.join("");
+
+ this.canvas.appendChild(this.cursor);
+ },
+
+ updateCursor : function(position)
+ {
+ var left = this.cursorLeft = position.column * this.characterWidth;
+ var top = this.cursorTop = position.row * this.lineHeight;
+
+ this.cursor.style.left = left + "px";
+ this.cursor.style.top = top + "px";
+
+ if (this.cursorVisible) {
+ this.canvas.appendChild(this.cursor);
+ }
+ },
+
+ hideCursor : function()
+ {
+ this.cursorVisible = true;
+ if (this.cursor.parentNode) {
+ this.cursor.parentNode.removeChild(this.cursor);
+ }
+ },
+
+ showCursor : function()
+ {
+ this.cursorVisible = true;
+ this.canvas.appendChild(this.cursor);
+ },
+
+ getScrollTop : function() {
+ return this.container.scrollTop;
+ },
+
+ scrollToY : function(scrollTop) {
+ return this.container.scrollTop = scrollTop;
+ },
+
+ scrollCursorIntoView : function()
+ {
+ var left = this.cursorLeft;
+ var top = this.cursorTop;
+
+ if (this.container.scrollLeft > left) {
+ this.container.scrollLeft = left;
+ }
+
+ if (this.container.scrollLeft + this.container.clientWidth < left + this.characterWidth) {
+ this.container.scrollLeft = left + this.characterWidth - this.container.clientWidth;
+ }
+
+ if (this.container.scrollTop > top) {
+ this.container.scrollTop = top;
+ }
+
+ if (this.container.scrollTop + this.container.clientHeight < top + this.lineHeight) {
+ this.container.scrollTop = top + this.lineHeight - this.container.clientHeight;
+ }
+ },
+
+ screenToTextCoordinates : function(pageX, pageY)
+ {
+ var canvasPos = this.container.getBoundingClientRect();
+
+ if (pageY < canvasPos.top || pageY > canvasPos.bottom) {
+ row = null;
+ } else {
+ var row = Math.floor((pageY + this.container.scrollTop - canvasPos.top) / this.lineHeight);
+ }
+
+ if (pageX < canvasPos.left || pageX > canvasPos.right) {
+ col = null;
+ } else {
+ var col = Math.floor((pageX + this.container.scrollLeft - canvasPos.left) / this.characterWidth);
+ }
+
+ return {
+ row: row,
+ column: col
+ }
+ },
+
+ visualizeFocus : function() {
+ this.container.className = "focus";
+ },
+
+ visualizeBlur : function() {
+ this.container.className = "";
+ },
+
+ showComposition : function(position)
+ {
+ setText(this.composition, "");
+
+ this.composition.style.left = (position.column * this.characterWidth+1) + "px";
+ this.composition.style.top = (position.row * this.lineHeight+1) + "px";
+
+ this.container.appendChild(this.composition);
+ },
+
+ setCompositionText : function(text) {
+ setText(this.composition, text);
+ },
+
+ hideComposition : function() {
+ if (this.composition.parentNode) {
+ this.container.removeChild(this.composition);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Editor.js b/Editor.js
new file mode 100644
index 00000000..cfc33be8
--- /dev/null
+++ b/Editor.js
@@ -0,0 +1,302 @@
+function TextInput(parentNode, host) {
+
+ var text = document.createElement("textarea");
+ var style = text.style;
+ style.position = "absolute";
+ style.left = "-10000px";
+ style.top = "-10000px";
+ parentNode.appendChild(text);
+
+ var inCompostion = false;
+
+ var onTextInput = function(e) {
+ setTimeout(function() {
+ if (!inCompostion) {
+ if (text.value) host.onTextInput(text.value);
+ text.value = "";
+ }
+ }, 0)
+ }
+
+ var onCompositionStart = function(e)
+ {
+ inCompostion = true;
+
+ if (text.value) host.onTextInput(text.value);
+ text.value = "";
+
+ host.onCompositionStart();
+ setTimeout(onCompositionUpdate, 0);
+ }
+
+ var onCompositionUpdate = function() {
+ host.onCompositionUpdate(text.value);
+ }
+
+ var onCompositionEnd = function()
+ {
+ inCompostion = false;
+ host.onCompositionEnd();
+ onTextInput();
+ }
+
+ addListener(text, "keypress", onTextInput, false);
+ addListener(text, "textInput", onTextInput, false);
+ addListener(text, "paste", onTextInput, false);
+ addListener(text, "propertychange", onTextInput, false);
+
+ addListener(text, "compositionstart", onCompositionStart, false);
+ addListener(text, "compositionupdate", onCompositionUpdate, false);
+ addListener(text, "compositionend", onCompositionEnd, false);
+
+ addListener(text, "blur", function() {
+ host.onBlur();
+ }, false);
+
+ addListener(text, "focus", function() {
+ host.onFocus();
+ }, false);
+
+
+ this.focus = function() {
+ text.focus();
+ }
+
+ this.blur = function() {
+ this.blur();
+ }
+};
+
+var keys = {
+ UP: 38,
+ RIGHT: 39,
+ DOWN: 40,
+ LEFT: 37,
+ POS1: 36,
+ END: 35,
+ DELETE: 46,
+ BACKSPACE: 8,
+ TAB: 9
+}
+
+function KeyBinding(element, host)
+{
+ addListener(element, "keydown", function(e)
+ {
+ var key = e.keyCode;
+
+ switch (key)
+ {
+ case keys.UP:
+ host.moveUp();
+ return stopEvent(e);
+
+ case keys.DOWN:
+ host.moveDown();
+ return stopEvent(e);
+
+ case keys.LEFT:
+ host.moveLeft();
+ return stopEvent(e);
+
+ case keys.RIGHT:
+ host.moveRight();
+ return stopEvent(e);
+
+ case keys.POS1:
+ host.moveLineStart();
+ return stopEvent(e);
+
+ case keys.END:
+ host.moveLineEnd();
+ return stopEvent(e);
+
+ case keys.DELETE:
+ host.removeRight();
+ return stopEvent(e);
+
+ case keys.BACKSPACE:
+ host.removeLeft();
+ return stopEvent(e);
+
+ case keys.TAB:
+ host.onTextInput(" ");
+ return stopEvent(e);
+ }
+ });
+};
+
+function Editor(doc, renderer)
+{
+ var container = renderer.getContainerElement();
+ this.renderer = renderer;
+
+ var textInput = new TextInput(container, this);
+ new KeyBinding(container, this);
+
+ var self = this;
+ addListener(container, "mousedown", function(e) {
+ textInput.focus();
+ self.placeCursorToMouse(e.pageX, e.pageY);
+ return preventDefault(e);
+ });
+
+ addListener(container, "mousewheel", function(e) {
+ var delta = e.wheelDeltaY;
+ self.renderer.scrollToY(self.renderer.getScrollTop() - (delta/10));
+ return preventDefault(e);
+ });
+
+ this.cursor = {
+ row: 0,
+ column: 0
+ }
+ this.doc = doc;
+ renderer.setDocument(doc);
+
+ this.draw();
+}
+
+Editor.prototype =
+{
+ draw : function()
+ {
+ this.renderer.draw();
+ this.renderer.updateCursor(this.cursor);
+ },
+
+ updateCursor : function() {
+ this.renderer.updateCursor(this.cursor);
+ },
+
+ onFocus : function() {
+ this.renderer.showCursor();
+ this.renderer.visualizeFocus();
+ },
+
+ onBlur : function() {
+ this.renderer.hideCursor();
+ this.renderer.visualizeBlur();
+ },
+
+ placeCursorToMouse : function(pageX, pageY)
+ {
+ var pos = this.renderer.screenToTextCoordinates(pageX, pageY);
+ this.moveTo(pos.row, pos.column);
+ },
+
+ onTextInput: function(text)
+ {
+ this.cursor = this.doc.insert(this.cursor, text);
+ this.draw();
+ this.renderer.scrollCursorIntoView();
+ },
+
+ removeRight : function()
+ {
+ var rangeEnd = {
+ row: this.cursor.row,
+ column: this.cursor.column + 1
+ }
+ if (rangeEnd.column > this.doc.getLine(this.cursor.row).length) {
+ rangeEnd.row += 1;
+ rangeEnd.column = 0;
+ }
+ this.doc.remove({start: this.cursor, end: renageEnd});
+
+ this.draw();
+ this.renderer.scrollCursorIntoView();
+ },
+
+ removeLeft : function()
+ {
+ if (this.cursor.row == 0 && this.cursor.column == 0) {
+ return;
+ }
+
+ var rangeStart = {
+ row: this.cursor.row,
+ column: this.cursor.column + -1
+ }
+ if (rangeStart.column < 0)
+ {
+ rangeStart.row -= 1;
+ rangeStart.column = this.doc.getLine(this.cursor.row-1).length;
+ }
+ this.cursor = this.doc.remove({start: rangeStart, end: this.cursor});
+
+ this.draw();
+ this.renderer.scrollCursorIntoView();
+ },
+
+ onCompositionStart : function()
+ {
+ this.renderer.showComposition(this.cursor);
+ this.onTextInput(" ");
+ },
+
+ onCompositionUpdate : function(text) {
+ this.renderer.setCompositionText(text);
+ },
+
+ onCompositionEnd : function() {
+ this.renderer.hideComposition();
+ this.removeLeft();
+ },
+
+ moveUp : function() {
+ this.moveBy(-1, 0);
+ this.renderer.scrollCursorIntoView();
+ },
+
+ moveDown : function() {
+ this.moveBy(1, 0);
+ this.renderer.scrollCursorIntoView();
+ },
+
+ moveLeft : function()
+ {
+ if (this.cursor.column == 0) {
+ if (this.cursor.row > 0) {
+ this.moveTo(this.cursor.row-1, this.doc.getLine(this.cursor.row-1).length);
+ }
+ } else {
+ this.moveBy(0, -1);
+ }
+ this.renderer.scrollCursorIntoView();
+ },
+
+ moveRight : function()
+ {
+ if (this.cursor.column == this.doc.getLine(this.cursor.row).length) {
+ if (this.cursor.row < this.doc.getLength()-1) {
+ this.moveTo(this.cursor.row+1, 0);
+ }
+ } else {
+ this.moveBy(0, 1);
+ }
+ this.renderer.scrollCursorIntoView();
+ },
+
+ moveLineStart : function()
+ {
+ this.moveTo(this.cursor.row, 0);
+ this.renderer.scrollCursorIntoView();
+ },
+
+ moveLineEnd : function() {
+ this.moveTo(this.cursor.row, this.doc.getLine(this.cursor.row).length);
+ this.renderer.scrollCursorIntoView();
+ },
+
+ moveBy : function(rows, chars) {
+ this.moveTo(this.cursor.row+rows, this.cursor.column+chars);
+ },
+
+ moveTo : function(row, column)
+ {
+ this.cursor.row = Math.min(this.doc.getLength()-1, Math.max(0, row));
+ this.cursor.column = Math.min(this.doc.getLine(this.cursor.row).length, Math.max(0, column));
+ this.updateCursor();
+ }
+}
\ No newline at end of file
diff --git a/TextDocument.js b/TextDocument.js
new file mode 100644
index 00000000..310f36ca
--- /dev/null
+++ b/TextDocument.js
@@ -0,0 +1,144 @@
+function TextDocument(text) {
+ this.lines = this._split(text);
+}
+
+TextDocument.prototype =
+{
+ _split : function(text) {
+ return text.split(/[\n\r]/)
+ },
+
+ getLine : function(row) {
+ return this.lines[row] || "";
+ },
+
+ keywords : {
+ "break" : 1,
+ "case" : 1,
+ "catch" : 1,
+ "continue" : 1,
+ "default" : 1,
+ "delete" : 1,
+ "do" : 1,
+ "else" : 1,
+ "finally" : 1,
+ "for" : 1,
+ "function" : 1,
+ "if" : 1,
+ "in" : 1,
+ "instanceof" : 1,
+ "new" : 1,
+ "return" : 1,
+ "switch" : 1,
+ "throw" : 1,
+ "try" : 1,
+ "typeof" : 1,
+ "var" : 1,
+ "while" : 1,
+ "with" : 1
+ },
+
+ getLineTokens : function(row)
+ {
+ var tokens = [];
+
+ var re = /(?:(\s+)|("[^"]*")|('[^']*')|([\[\]\(\)\{\}])|([a-zA-Z_][a-zA-Z0-9_]*)|(\/\/.*)|(.))/g
+ re.lastIndex = 0;
+
+ var match;
+ var line = this.getLine(row);
+ while (match = re.exec(line))
+ {
+ var token = {
+ type: "text",
+ value: match[0]
+ }
+
+ if (match[2] || match[3]) {
+ token.type = "string";
+ } else if (match[5] && this.keywords[match[5]]) {
+ token.type = "keyword";
+ } else if (match[6]) {
+ token.type = "comment";
+ }
+
+ tokens.push(token);
+ };
+
+ return tokens;
+ },
+
+ getLength : function() {
+ return this.lines.length;
+ },
+
+ insert : function(position, text)
+ {
+ var newLines = this._split(text);
+
+ if (text == "\n")
+ {
+ var line = this.lines[position.row] || "";
+ this.lines[position.row] = line.substring(0, position.column);
+ this.lines.splice(position.row+1, 0, line.substring(position.column));
+
+ return {
+ row: position.row + 1,
+ column: 0
+ };
+ }
+ else if (newLines.length == 1)
+ {
+ var line = this.lines[position.row] || "";
+ this.lines[position.row] = line.substring(0, position.column) + text + line.substring(position.column);
+
+ return {
+ row: position.row,
+ column: position.column+text.length
+ }
+ }
+ else
+ {
+ var line = this.lines[position.row] || "";
+
+ this.lines[position.row] = line.substring(0, position.column) + newLines[0];
+ this.lines[position.row+1] = newLines[newLines.length-1] + line.substring(position.column);
+
+ if (newLines.length > 2)
+ {
+ var args = [position.row + 1, 0]
+ args.push.apply(args, newLines.slice(1, -1));
+ this.lines.splice.apply(this.lines, args);
+ }
+
+ return {
+ row: position.row + newLines.length - 1,
+ column: newLines[newLines.length-1].length
+ };
+ }
+ },
+
+ remove : function(range)
+ {
+ var firstRow = range.start.row;
+ var lastRow = range.end.row;
+
+ var row =
+ this.lines[firstRow].substring(0, range.start.column) +
+ this.lines[lastRow].substring(range.end.column);
+
+ this.lines.splice(firstRow, lastRow-firstRow+1, row);
+
+ return range.start;
+ },
+
+ replace : function(range, text)
+ {
+ this.remove(range);
+ if (text) {
+ return this.insert(range.start, text);
+ } else {
+ return range.start;
+ }
+ }
+}
\ No newline at end of file
diff --git a/VirtualRenderer.js b/VirtualRenderer.js
new file mode 100644
index 00000000..0b5ec321
--- /dev/null
+++ b/VirtualRenderer.js
@@ -0,0 +1,144 @@
+function VirtualRenderer(containerId)
+{
+ DumbRenderer.call(this, containerId);
+ this.scrollTop = 0;
+ this.firstRow = 0;
+
+ this.cursorPos = {
+ row: 0,
+ column: 0
+ };
+}
+inherits(VirtualRenderer, DumbRenderer);
+
+VirtualRenderer.prototype.draw = function()
+{
+ var lines = this.lines;
+
+ var offset = this.scrollTop % this.lineHeight;
+ var minHeight = this.container.clientHeight + offset;
+
+ var longestLine = this.getLongestLineWidth(lines);
+
+ this.canvas.style.marginTop = (-offset) + "px";
+ this.canvas.style.height = minHeight + "px";
+ this.canvas.style.width = longestLine + "px";
+
+ var lineCount = Math.ceil(minHeight / this.lineHeight);
+ this.firstRow = firstRow = Math.round((this.scrollTop - offset) / this.lineHeight);
+ var lastRow = Math.min(lines.length, firstRow+lineCount);
+
+ var html = [];
+ for (var i=firstRow; i"
+ );
+ this.renderLine(html, i),
+ html.push("
");
+ }
+
+ this.canvas.innerHTML = html.join("");
+
+ this.updateCursor(this.cursorPos);
+}
+
+VirtualRenderer.prototype.renderLine = function(stringBuilder, row)
+{
+ var tokens = this.doc.getLineTokens(row);
+ for (var i=0; i < tokens.length; i++)
+ {
+ var token = tokens[i];
+
+ var output = token.value.
+ replace(/&/g, "&").
+ replace(/", output, "");
+ } else {
+ stringBuilder.push(output);
+ }
+ };
+};
+
+VirtualRenderer.prototype.updateCursor = function(position)
+{
+ this.cursorPos = {
+ row: position.row,
+ column: position.column
+ }
+
+ var left = this.cursorLeft = position.column * this.characterWidth;
+ var top = this.cursorTop = position.row * this.lineHeight;
+
+ this.cursor.style.left = left + "px";
+ this.cursor.style.top = (top - (this.firstRow * this.lineHeight)) + "px";
+
+ if (this.cursorVisible) {
+ this.canvas.appendChild(this.cursor);
+ }
+};
+
+VirtualRenderer.prototype.scrollCursorIntoView = function()
+{
+ var left = this.cursorLeft;
+ var top = this.cursorTop;
+
+ if (this.getScrollTop() > top) {
+ this.scrollToY(top);
+ }
+
+ if (this.getScrollTop() + this.container.clientHeight < top + this.lineHeight) {
+ this.scrollToY(top + this.lineHeight - this.container.clientHeight);
+ }
+
+ if (this.container.scrollLeft > left) {
+ this.container.scrollLeft = left;
+ }
+
+ if (this.container.scrollLeft + this.container.clientWidth < left + this.characterWidth) {
+ this.container.scrollLeft = left + this.characterWidth - this.container.clientWidth;
+ }
+},
+
+VirtualRenderer.prototype.getScrollTop = function() {
+ return this.scrollTop;
+};
+
+VirtualRenderer.prototype.scrollToY = function(scrollTop)
+{
+ var maxHeight = this.lines.length * this.lineHeight - this.container.offsetHeight;
+ var scrollTop = Math.max(0, Math.min(maxHeight, scrollTop));
+
+ if (this.scrollTop !== scrollTop) {
+ this.scrollTop = scrollTop;
+ this.draw();
+ }
+};
+
+VirtualRenderer.prototype.screenToTextCoordinates = function(pageX, pageY)
+{
+ var canvasPos = this.container.getBoundingClientRect();
+
+ if (pageX < canvasPos.left || pageX > canvasPos.right) {
+ col = null;
+ } else {
+ var col = Math.floor((pageX + this.container.scrollLeft - canvasPos.left) / this.characterWidth);
+ }
+
+ if (pageY < canvasPos.top || pageY > canvasPos.bottom) {
+ row = null;
+ } else {
+ var row = Math.floor((pageY + this.scrollTop - canvasPos.top) / this.lineHeight);
+ }
+
+ return {
+ row: row,
+ column: col
+ }
+};
\ No newline at end of file
diff --git a/editor.html b/editor.html
index b7d6b263..837af221 100644
--- a/editor.html
+++ b/editor.html
@@ -11,6 +11,7 @@
#virtual_container {
position: absolute;
+ padding: 3px;
border: 1px solid black;
overflow-x: auto;
overflow-y: hidden;
@@ -38,7 +39,8 @@
.canvas {
position: absolute;
overflow: hidden;
- font-family: Courier New;
+ font-family: Monaco, "Courier New";
+ font-size: 14px;
white-space: nowrap;
-webkit-box-sizing: border-box;
box-sizing: border-box;
@@ -63,8 +65,25 @@
background: #FAFAFA;
}
+ .keyword {
+ color: blue;
+ }
+
+ .string {
+ color: rgb(3, 106, 7);
+ }
+
+ .comment {
+ font-style: italic;
+ color: rgb(0, 102, 255);
+ }
+
+
+
+
+
@@ -75,709 +94,11 @@
diff --git a/lib.js b/lib.js
new file mode 100644
index 00000000..d839e70e
--- /dev/null
+++ b/lib.js
@@ -0,0 +1,48 @@
+function addListener(elem, type, callback) {
+ if (elem.addEventListener) {
+ return elem.addEventListener(type, callback, false);
+ }
+ if (elem.attachEvent) {
+ elem.attachEvent("on" + type, function() {
+ callback(window.event);
+ });
+ }
+}
+
+function setText(elem, text) {
+ if (elem.innerText !== undefined) {
+ elem.innerText = text;
+ }
+ if (elem.textContent !== undefined) {
+ elem.textContent = text;
+ }
+}
+
+function stopEvent(e) {
+ stopPropagation(e);
+ preventDefault(e);
+ return false;
+}
+
+function stopPropagation(e) {
+ if (e.stopPropagation)
+ e.stopPropagation();
+ else
+ e.cancelBubble = true;
+}
+
+function preventDefault (e)
+{
+ if (e.preventDefault)
+ e.preventDefault();
+ else
+ e.returnValue = false;
+}
+
+inherits = function (ctor, superCtor) {
+ var tempCtor = function(){};
+ tempCtor.prototype = superCtor.prototype;
+ ctor.super_ = superCtor.prototype;
+ ctor.prototype = new tempCtor();
+ ctor.prototype.constructor = ctor;
+};
\ No newline at end of file