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