split code into several files
This commit is contained in:
parent
76aef02db4
commit
828b4a184a
6 changed files with 850 additions and 702 deletions
189
DumbRenderer.js
Normal file
189
DumbRenderer.js
Normal file
|
|
@ -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<br>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(
|
||||||
|
"<div class='line ",
|
||||||
|
i % 2 == 0 ? "even" : "odd",
|
||||||
|
"' style='height:" + this.lineHeight + "px;",
|
||||||
|
"width:", longestLine, "px'>",
|
||||||
|
lines[i].
|
||||||
|
replace(/&/g, "&").
|
||||||
|
replace(/</g, "<").
|
||||||
|
replace(/\s/g, " "),
|
||||||
|
"</div>"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
302
Editor.js
Normal file
302
Editor.js
Normal file
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
144
TextDocument.js
Normal file
144
TextDocument.js
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
144
VirtualRenderer.js
Normal file
144
VirtualRenderer.js
Normal file
|
|
@ -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<lastRow; i++)
|
||||||
|
{
|
||||||
|
html.push(
|
||||||
|
"<div class='line ",
|
||||||
|
i % 2 == 0 ? "even" : "odd",
|
||||||
|
"' style='height:" + this.lineHeight + "px;",
|
||||||
|
"width:", longestLine, "px'>"
|
||||||
|
);
|
||||||
|
this.renderLine(html, i),
|
||||||
|
html.push("</div>");
|
||||||
|
}
|
||||||
|
|
||||||
|
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(/</g, "<").
|
||||||
|
replace(/\s/g, " ");
|
||||||
|
|
||||||
|
if (token.type !== "text") {
|
||||||
|
stringBuilder.push("<span class='", token.type, "'>", output, "</span>");
|
||||||
|
} 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
|
||||||
|
}
|
||||||
|
};
|
||||||
725
editor.html
725
editor.html
|
|
@ -11,6 +11,7 @@
|
||||||
|
|
||||||
#virtual_container {
|
#virtual_container {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
padding: 3px;
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
|
|
@ -38,7 +39,8 @@
|
||||||
.canvas {
|
.canvas {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-family: Courier New;
|
font-family: Monaco, "Courier New";
|
||||||
|
font-size: 14px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
-webkit-box-sizing: border-box;
|
-webkit-box-sizing: border-box;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
@ -63,8 +65,25 @@
|
||||||
background: #FAFAFA;
|
background: #FAFAFA;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.keyword {
|
||||||
|
color: blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
.string {
|
||||||
|
color: rgb(3, 106, 7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment {
|
||||||
|
font-style: italic;
|
||||||
|
color: rgb(0, 102, 255);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<script src="lib.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="TextDocument.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="Editor.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="DumbRenderer.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="VirtualRenderer.js" type="text/javascript" charset="utf-8"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
|
@ -76,708 +95,10 @@
|
||||||
|
|
||||||
<script type="text/javascript" charset="utf-8">
|
<script type="text/javascript" charset="utf-8">
|
||||||
|
|
||||||
function addListener(elem, type, callback) {
|
var doc = new TextDocument("Juhu Kinners");
|
||||||
if (elem.addEventListener) {
|
|
||||||
return elem.addEventListener(type, callback, false);
|
|
||||||
}
|
|
||||||
if (elem.attachEvent) {
|
|
||||||
elem.attachEvent("on" + type, function() {
|
|
||||||
callback(window.event);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setText(elem, text) {
|
new Editor(doc, new VirtualRenderer("virtual_container"));
|
||||||
if (elem.innerText !== undefined) {
|
new Editor(doc, new DumbRenderer("container"));
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
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(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.row = 0;
|
|
||||||
this.col = 0;
|
|
||||||
this.lines = [""];
|
|
||||||
renderer.setLines(this.lines);
|
|
||||||
|
|
||||||
this.draw();
|
|
||||||
}
|
|
||||||
|
|
||||||
Editor.prototype =
|
|
||||||
{
|
|
||||||
draw : function()
|
|
||||||
{
|
|
||||||
this.renderer.draw();
|
|
||||||
this.renderer.updateCursor(this.row, this.col);
|
|
||||||
},
|
|
||||||
|
|
||||||
updateCursor : function() {
|
|
||||||
this.renderer.updateCursor(this.row, this.col);
|
|
||||||
},
|
|
||||||
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
var newLines = text.split(/[\n\r]/);
|
|
||||||
|
|
||||||
if (text == "\n")
|
|
||||||
{
|
|
||||||
var line = this.lines[this.row] || "";
|
|
||||||
this.lines[this.row] = line.substring(0, this.col);
|
|
||||||
this.lines.splice(this.row+1, 0, line.substring(this.col));
|
|
||||||
|
|
||||||
this.row += 1;
|
|
||||||
this.col = 0;
|
|
||||||
}
|
|
||||||
else if (newLines.length == 1)
|
|
||||||
{
|
|
||||||
var line = this.lines[this.row] || "";
|
|
||||||
this.lines[this.row] = line.substring(0, this.col) + text + line.substring(this.col);
|
|
||||||
this.col += text.length;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var line = this.lines[this.row] || "";
|
|
||||||
|
|
||||||
this.lines[this.row] = line.substring(0, this.col) + newLines[0];
|
|
||||||
this.lines[this.row+1] = newLines[newLines.length-1] + line.substring(this.col);
|
|
||||||
|
|
||||||
if (newLines.length > 2)
|
|
||||||
{
|
|
||||||
var args = [this.row + 1, 0]
|
|
||||||
args.push.apply(args, newLines.slice(1, -1));
|
|
||||||
this.lines.splice.apply(this.lines, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.row = this.row + newLines.length - 1;
|
|
||||||
this.col = newLines[newLines.length-1].length;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.draw();
|
|
||||||
this.renderer.scrollCursorIntoView();
|
|
||||||
},
|
|
||||||
|
|
||||||
removeRight : function()
|
|
||||||
{
|
|
||||||
var currentLine = this.lines[this.row];
|
|
||||||
|
|
||||||
if (this.col == currentLine.length)
|
|
||||||
{
|
|
||||||
this.lines[this.row] = currentLine + (this.lines[this.row+1] || "");
|
|
||||||
this.lines.splice(this.row+1, 1);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
this.lines[this.row] = currentLine.substring(0, this.col) + currentLine.substring(this.col + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.draw();
|
|
||||||
this.renderer.scrollCursorIntoView();
|
|
||||||
},
|
|
||||||
|
|
||||||
removeLeft : function()
|
|
||||||
{
|
|
||||||
var currentLine = this.lines[this.row] || "";
|
|
||||||
|
|
||||||
if (this.col == 0)
|
|
||||||
{
|
|
||||||
if (this.row !== 0)
|
|
||||||
{
|
|
||||||
var prevLine = this.lines[this.row-1] || ""
|
|
||||||
this.lines[this.row-1] = prevLine + currentLine;
|
|
||||||
this.lines.splice(this.row, 1);
|
|
||||||
this.row -= 1;
|
|
||||||
this.col = prevLine.length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
this.lines[this.row] = currentLine.substring(0, this.col-1) + currentLine.substring(this.col);
|
|
||||||
this.col -= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.draw();
|
|
||||||
this.renderer.scrollCursorIntoView();
|
|
||||||
},
|
|
||||||
|
|
||||||
onCompositionStart : function()
|
|
||||||
{
|
|
||||||
this.renderer.showComposition(this.row, this.col);
|
|
||||||
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.col == 0) {
|
|
||||||
if (this.row > 0) {
|
|
||||||
this.moveTo(this.row-1, this.lines[this.row-1].length);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.moveBy(0, -1);
|
|
||||||
}
|
|
||||||
this.renderer.scrollCursorIntoView();
|
|
||||||
},
|
|
||||||
|
|
||||||
moveRight : function()
|
|
||||||
{
|
|
||||||
if (this.col == this.lines[this.row].length) {
|
|
||||||
if (this.row < this.lines.length-1) {
|
|
||||||
this.moveTo(this.row+1, 0);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.moveBy(0, 1);
|
|
||||||
}
|
|
||||||
this.renderer.scrollCursorIntoView();
|
|
||||||
},
|
|
||||||
|
|
||||||
moveLineStart : function()
|
|
||||||
{
|
|
||||||
this.moveTo(this.row, 0);
|
|
||||||
this.renderer.scrollCursorIntoView();
|
|
||||||
},
|
|
||||||
|
|
||||||
moveLineEnd : function() {
|
|
||||||
this.moveTo(this.row, this.lines[this.row].length);
|
|
||||||
this.renderer.scrollCursorIntoView();
|
|
||||||
},
|
|
||||||
|
|
||||||
moveBy : function(rows, chars) {
|
|
||||||
this.moveTo(this.row+rows, this.col+chars);
|
|
||||||
},
|
|
||||||
|
|
||||||
moveTo : function(row, column)
|
|
||||||
{
|
|
||||||
this.row = Math.min(this.lines.length-1, Math.max(0, row));
|
|
||||||
this.col = Math.min(this.lines[this.row].length, Math.max(0, column));
|
|
||||||
this.updateCursor();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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 =
|
|
||||||
{
|
|
||||||
setLines : function(lines) {
|
|
||||||
this.lines = lines;
|
|
||||||
},
|
|
||||||
|
|
||||||
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<br>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(
|
|
||||||
"<div class='line ",
|
|
||||||
i % 2 == 0 ? "even" : "odd",
|
|
||||||
"' style='height:" + this.lineHeight + "px;",
|
|
||||||
"width:", longestLine, "px'>",
|
|
||||||
lines[i].
|
|
||||||
replace(/&/g, "&").
|
|
||||||
replace(/</g, "<").
|
|
||||||
replace(/\s/g, " "),
|
|
||||||
"</div>"
|
|
||||||
);
|
|
||||||
};
|
|
||||||
this.canvas.innerHTML = html.join("");
|
|
||||||
|
|
||||||
this.canvas.appendChild(this.cursor);
|
|
||||||
},
|
|
||||||
|
|
||||||
updateCursor : function(row, column)
|
|
||||||
{
|
|
||||||
var left = this.cursorLeft = column * this.characterWidth;
|
|
||||||
var top = this.cursorTop = 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(row, column)
|
|
||||||
{
|
|
||||||
setText(this.composition, "");
|
|
||||||
|
|
||||||
this.composition.style.left = (column * this.characterWidth+1) + "px";
|
|
||||||
this.composition.style.top = (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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function VirtualRenderer(containerId)
|
|
||||||
{
|
|
||||||
DumbRenderer.call(this, containerId);
|
|
||||||
this.scrollTop = 0;
|
|
||||||
this.firstRow = 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<lastRow; i++)
|
|
||||||
{
|
|
||||||
html.push(
|
|
||||||
"<div class='line ",
|
|
||||||
i % 2 == 0 ? "even" : "odd",
|
|
||||||
"' style='height:" + this.lineHeight + "px;",
|
|
||||||
"width:", longestLine, "px'>",
|
|
||||||
lines[i].
|
|
||||||
replace(/&/g, "&").
|
|
||||||
replace(/</g, "<").
|
|
||||||
replace(/\s/g, " "),
|
|
||||||
"</div>"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.canvas.innerHTML = html.join("");
|
|
||||||
|
|
||||||
this.updateCursor(this.cursorRow, this.cursorColumn);
|
|
||||||
}
|
|
||||||
|
|
||||||
VirtualRenderer.prototype.updateCursor = function(row, column)
|
|
||||||
{
|
|
||||||
this.cursorRow = row;
|
|
||||||
this.cursorColumn = column;
|
|
||||||
|
|
||||||
var left = this.cursorLeft = column * this.characterWidth;
|
|
||||||
var top = this.cursorTop = row * this.lineHeight;
|
|
||||||
|
|
||||||
this.cursor.style.left = left + "px";
|
|
||||||
this.cursor.style.top = (top - (this.firstRow * this.lineHeight)) + "px";
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
new Editor(new VirtualRenderer("virtual_container"));
|
|
||||||
new Editor(new DumbRenderer("container"));
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
48
lib.js
Normal file
48
lib.js
Normal file
|
|
@ -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;
|
||||||
|
};
|
||||||
Loading…
Add table
Add a link
Reference in a new issue