Skip to content

Instantly share code, notes, and snippets.

@sdawson
Forked from josephg/gist:972894
Created May 15, 2011 05:33
Show Gist options
  • Save sdawson/972901 to your computer and use it in GitHub Desktop.
Save sdawson/972901 to your computer and use it in GitHub Desktop.
Sarah's ot code
<!DOCTYPE HTML>
<html>
<head>
<title>Wave textbox test page</title>
<meta charset="utf-8">
<style>
body {
width: 800px;
margin: auto;
}
textarea {
height: 400px;
width: 800px;
}
</style>
</head>
<body>
<h2>Testing, testing, 1, 2, 3...</h3>
<h4>Example textarea:</h4>
<textarea id="whatnot-textarea"></textarea>
<script type="text/javascript" src="http://192.168.0.51:8000/socket.io/socket.io.js"></script>
<script type="text/javascript" src="http://192.168.0.51/~josephg/webclient.js"></script>
<script>
var doc;
function setUp() {
var connection1 = new whatnot.Connection("192.168.0.51", 8000);
connection1.getOrCreate("sarahtest", 'text', function (doc, error) {
getOps(doc, "whatnot-textarea");
});
}
function getOps(doc, testAreaId) {
var textElem = document.getElementById(testAreaId), //the element containing the text being edited
selectedText = "", //records the selected text
selectedLen = 0; //records the length of selected text
sub = false;
textElem.spellcheck = false;
textElem.value = doc.snapshot;
doc.onChanged(update);
function update(op) {
var cursorPos = getCursor();
getSelected();
var selecEnd = document.activeElement.selectionEnd;
for (var x=0; x<op.length; x++) {
if (op[x].i) {
processInput(op[x].i, op[x].p, 0);
console.log("Op applied");
console.log(op);
if (cursorPos >= op[x].p) { //cursor & selection are after inserted text
cursorPos = cursorPos + op[x].i.length;
} else if (cursorPos < op[x].p && cursorPos + selectedLen > op[x].p) { //selection over area where insert applied
selectedLen = selectedLen + op[x].i.length;
}
} else if (op[x].d) {
textElem.value = textElem.value.substring(0, op[x].p) + textElem.value.substring(op[x].d.length + op[x].p);
console.log("Op applied");
console.log(op);
if (selecEnd <= op[x].p) { //cursor & selection are before deleted text: no change required
} else if (cursorPos >= op[x].p + op[x].d.length) { //cursor & selection are after deleted text
cursorPos = cursorPos - op[x].d.length;
} else if (cursorPos > op[x].p && cursorPos <= op[x].p + op[x].d.length && selecEnd > op[x].p + op[x].d.length) {
//selection overlaps end of deleted text
selectedLen = selectedLen - (op[x].p + op[x].d.length - cursorPos);
cursorPos = op[x].p;
} else if (selecEnd > op[x].p && selecEnd < op[x].p + op[x].d.length && cursorPos <= op[x].p) {
//selection overlaps start of deleted text
selectedLen = op[x].p - cursorPos;
} else if (cursorPos <= op[x].p && selecEnd >= op[x].p + op[x].d.length) { //selection overlaps all of deleted text
selectedLen = selectedLen - op[x].d.length;
} else if (cursorPos >= op[x].p && selecEnd <= op[x].p + op[x].d.length) { //selection entirely contained in deleted text
cursorPos = op[x].p;
selectedLen = 0;
}
} else {
console.error("Invalid op: neither insert nor delete");
}
}
selecEnd = cursorPos + selectedLen;
setCursor(cursorPos, selecEnd);
}
//set up event listeners...
textElem.addEventListener("textInput", getInputText, false);
textElem.addEventListener("keydown", onKeyDown, false);
textElem.addEventListener("keyup", domChanged, false);
textElem.addEventListener("select", getSelected, false);
textElem.addEventListener("cut", doCut, false);
textElem.addEventListener("paste", doPaste, false);
textElem.addEventListener("copy", doCopy, false);
//handler function: on a textInput event, records a string of input text into insertText
function getInputText(event) {
getSelected();
event.preventDefault();
var insertText = event.data;
var cursorPos = getCursor();
processInput(insertText, cursorPos, selectedLen);
setCursor(cursorPos + insertText.length, cursorPos + insertText.length);
if (selectedText) {
sendCombinedOp(insertText, cursorPos);
} else {
sendInsertOp(insertText, cursorPos);
}
}
function processInput(insertText, cursorPos, offset) {
if (insertText.length > 1) { //this deals with Windows newlines
insertText = insertText.replace(/\r/g, "");
}
textElem.value = textElem.value.substring(0, cursorPos) + insertText + textElem.value.substring(cursorPos + offset);
console.log("Insert text is: ", insertText);
}
function onKeyDown(event) {
sub = false;
if (event.keyCode === 8 || event.keyCode === 46) {
getTextToBeDeleted(event);
} else if (event.keyCode === 90 && event.ctrlKey === true) { //Note: none of this works to fix undo & redo!
sendUndo(event);
} else if (event.keyCode === 89 && event.ctrlKey === true) {
sendRedo(event);
}
}
function getTextToBeDeleted(event) {
// getSelected();
// console.log(selectedText);
// console.log(selectedLen);
event.preventDefault();
var deletedText = "";
var cursorPos = getCursor();
if (selectedText) {
deletedText = selectedText;
console.log("Delete text is: ", deletedText);
sendDeleteOp("delete", deletedText);
} else if (event.keyCode === 8 && cursorPos !== 0) { //on backspace, and not at start of doc
cursorPos = cursorPos-1;
deletedText = textElem.value[cursorPos];
console.log("Delete text is: ", deletedText);
sendDeleteOp("backspace", deletedText);
} else if (event.keyCode === 46 && cursorPos !== textElem.value.length) { //on delete, and not at end of doc
deletedText = textElem.value[cursorPos];
console.log("Delete text is: ", deletedText);
sendDeleteOp("delete", deletedText);
} else {return;}
textElem.value = textElem.value.substring(0, cursorPos) + textElem.value.substring(cursorPos + deletedText.length);
setCursor(cursorPos, cursorPos);
}
function domChanged(event) {
/* var compareDoc = doc.snapshot;
var compareDocRep = compareDoc.replace(/\r/g, "");
var windowsSucks = textElem.value.replace(/\r/g, "");*/
if (sub === true && doc.snapshot !== textElem.value) {
console.error("ERROR: Document mismatch");
console.error("Document: ", escape(doc.snapshot));
console.error("TextElem.value: ", escape(textElem.value));
}
/* for (var i=0; i<compareDoc.length; i++) {
if (compareDoc[i] !== windowsSucks[i]) {
console.error("Character mismatch at position " + i);
console.error(compareDoc[i]);
console.error(windowsSucks[i]);
}
}
}*/
}
/* function onClick(event) {
}
*/
function getSelected(event) {
selectedText = textElem.value.substring(document.activeElement.selectionStart, document.activeElement.selectionEnd);
selectedLen = document.activeElement.selectionEnd - document.activeElement.selectionStart;
}
function doCut(event) {
getSelected();
// console.log("Cut event! Whoo!");
// console.log(selectedText);
deletedText = selectedText;
sendDeleteOp("delete", deletedText);
}
function doPaste(event) {
getSelected();
// console.log("Paste event! Whoo!");
// console.log(event);
}
function doCopy(event) {
// console.log("Copy event! Whoo!");
selectedText = "";
selectedLen = 0;
}
function sendUndo(event) {
event.preventDefault();
console.log("Undo caught");
}
function sendRedo(event) {
event.preventDefault();
console.log("Redo caught");
}
function getCursor() {
return textElem.selectionStart;
}
function setCursor(cursorPos, selecEnd) {
if (textElem.setSelectionRange) {
textElem.focus();
textElem.setSelectionRange(cursorPos, selecEnd);
} else if (textElem.createTextRange) {
var range = textElem.createTextRange();
range.collapse(true);
range.moveEnd('character', cursorPos);
range.moveStart('character', selecEnd);
range.select();
}
// console.log("??" + cursorPos);
}
//functions to send insert and delete ops
function sendInsertOp(insertText, cursorPos) {
var op = [{i: insertText, p: cursorPos}];
doc.submitOp(op, doc.version);
printOp(op);
}
function sendDeleteOp(type, deletedText) {
var op;
var cursorPos = getCursor();
if (type === "backspace") {
op = [{d: deletedText, p: cursorPos - 1}];
} else { //if (type === "delete") {
op = [{d: deletedText, p: cursorPos}];
}
selectedText = "";
selectedLen = 0;
doc.submitOp(op, doc.version);
printOp(op);
}
function sendCombinedOp(insertText, cursorPos) {
var op = [{d: selectedText, p: cursorPos}, {i: insertText, p: cursorPos}];
selectedText = "";
selectedLen = 0;
doc.submitOp(op, doc.version);
printOp(op);
}
function printOp(op) {
// console.log("Applying the op ");
// console.log(op);
// console.log(" to ", escape(textElem.value));
sub = true;
console.log("Doc.snapshot is: ")
console.log(doc.snapshot);
// console.log("Comparedoc: ", escape(compareDoc));
// console.log("Document now reads: \"" + compareDoc + "\""); //commented out while working offline
}
}
setUp();
/*
Not dealt with yet:
- cross browser compatability (urgh)
- getting external ops
- clean up the yuk
- undo/redo in browser
- how the freak do I deal with ppl choosing delete/undo/redo from the right click menu??
*/
</script>
</body>
</html>
@josephg
Copy link

josephg commented Jun 11, 2011

Hows this going?

@sdawson
Copy link
Author

sdawson commented Jun 12, 2011 via email

@josephg
Copy link

josephg commented Jun 12, 2011

Sure. :)

I was thinking of writing a catchall thing for other browsers where it basically does a super naive diffing algorithm, and run that on every event.

Say the document contains 'abcde', and I select the 'd' and change it to 'abcDe', you could detect that by trimming from the front until the characters are different (so, 'de' vs 'De') and scanning from the back until the characters are different ('d' vs 'D') and then making an op which deletes the 'd' and inserts a 'D'. In javascript, I think strings with the same contents are guaranteed to be the same string, so all the input events which don't change the document contents should be able to be skipped easily. I don't know how thats implemented, but I'm pretty sure thats part of the spec.

It'll be slow with big documents, but it should work everywhere. And it'll be fine for the millions of small strings like chat messages and stuff. And anyway, the code you're writing will replace it in proper browsers anyway.

I'm just not sure about dragging text around in text boxes. Maybe special case that? But yeah... what do you think? That should work, right?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment