-
-
Save sdawson/972901 to your computer and use it in GitHub Desktop.
<!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> | |
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?
Hows this going?