define([ '/common/cursor-treesome.js', '/bower_components/rangy/rangy-core.min.js' ], function (Tree, Rangy) { var verbose = function (x) { if (window.verboseMode) { console.log(x); } }; /* accepts the document used by the editor */ var Cursor = function (inner) { var cursor = {}; var getTextNodeValue = function (el) { if (!el.data) { return; } // We want to transform html entities into their code (non-breaking spaces into $ ) var div = document.createElement('div'); div.innerText = el.data; return div.innerHTML; }; // Store the cursor position as an offset from the beginning of the text HTML content var offsetRange = cursor.offsetRange = { start: 0, end: 0 }; // Get the length of the opening tag of an node (<body class="cp"> ==> 17) var getOpeningTagLength = function (node) { if (node.nodeType === node.TEXT_NODE) { return 0; } var html = node.outerHTML; var tagRegex = /^(<\s*[a-zA-Z-]*[^>]*>)(.+)/; var match = tagRegex.exec(html); var res = match && match.length > 1 ? match[1].length : 0; return res; }; // Get the offset recursively. We start with <body> and continue following the // path to the range var offsetInNode = function (element, offset, path, range) { if (path.length === 0) { offset += getOpeningTagLength(range.el); if (range.el.nodeType === range.el.TEXT_NODE) { var div = document.createElement('div'); div.innerText = range.el.data.slice(0, range.offset); return offset + div.innerHTML.length; } return offset + range.offset; } offset += getOpeningTagLength(element); for (var i = 0; i < element.childNodes.length; i++) { if (element.childNodes[i] === path[0]) { return offsetInNode(path.shift(), offset, path, range); } // It is not yet our path, add the length of the text node or tag's outerHTML offset += (getTextNodeValue(element.childNodes[i]) || element.childNodes[i].outerHTML).length; } }; // Get the cursor position as a range and transform it into // an offset from the beginning of the outer HTML var getOffsetFromRange = function (element) { var doc = element.ownerDocument || element.document; var win = doc.defaultView || doc.parentWindow; var o = { start: 0, end: 0 }; if (typeof win.getSelection !== "undefined") { var sel = win.getSelection(); if (sel.rangeCount > 0) { var range = win.getSelection().getRangeAt(0); // Do it for both start and end ['start', 'end'].forEach(function (t) { var inNode = { el: range[t + 'Container'], offset: range[t + 'Offset'] }; while (inNode.el.nodeType !== Node.TEXT_NODE && inNode.el.childNodes.length > inNode.offset) { inNode.el = inNode.el.childNodes[inNode.offset]; inNode.offset = 0; } var current = inNode.el; var path = []; while (current !== element) { path.unshift(current); current = current.parentNode; } if (current === element) { // Should always be the case o[t] = offsetInNode(current, 0, path, inNode); } else { console.error('???'); } }); } } return o; }; // Update the value of the offset // This should be called before applying changes to the document cursor.offsetUpdate = function () { try { var range = getOffsetFromRange(inner); offsetRange.start = range.start; offsetRange.end = range.end; } catch (e) { console.error(e); } }; // Transform the offset value using the operations from the diff // between the old and the new states of the document. var offsetTransformRange = function (offset, ops) { var transformCursor = function (cursor, op) { if (!op) { return cursor; } var pos = op.offset; var remove = op.toRemove; var insert = op.toInsert.length; if (typeof cursor === 'undefined') { return; } if (typeof remove === 'number' && pos < cursor) { cursor -= Math.min(remove, cursor - pos); } if (typeof insert === 'number' && pos < cursor) { cursor += insert; } return cursor; }; var c = offset; if (Array.isArray(ops)) { for (var i = ops.length - 1; i >= 0; i--) { c = transformCursor(c, ops[i]); } offset = c; } return offset; }; // Get the range starting from <body> and the offset value. // We substract length of HTML content to the offset until we reach a text node or 0. // If we reach a text node, it means we're in the final possible child and the // current valu of the offset is the range one. // If we reach 0 or a negative value, it means the range in is the current tag // and we should use offset 0. var getFinalRange = function (el, offset) { if (el.nodeType === el.TEXT_NODE) { // This should be the final text node var txt = document.createElement("textarea"); txt.appendChild(el.cloneNode()); txt.innerHTML = txt.innerHTML.slice(0, offset); return { el: el, offset: txt.value.length }; } if (el.tagName === 'BR') { // If the range is in a <br>, we have a brFix that will make it better later return { el: el, offset: 0 }; } // Remove the current tag opening length offset = offset - getOpeningTagLength(el); if (offset <= 0) { // Return the current node... return { el: el, offset: 0 }; } // For each child, if they length is greater than the current offset, they are // containing the range element we're looking for. // Otherwise, our range element is in a later sibling and we can just substract // their length. var newOffset = offset; for (var i = 0; i < el.childNodes.length; i++) { try { newOffset -= (getTextNodeValue(el.childNodes[i]) || el.childNodes[i].outerHTML).length; } catch (e) { console.log(el); console.log(el.childNodes[i]); } if (newOffset <= 0) { return getFinalRange(el.childNodes[i], offset); } offset = newOffset; } // New offset ends up in the closing tag // ==> return the last child... if (el.childNodes.length) { return getFinalRange(el.childNodes[el.childNodes.length - 1], offset); } else { return { el: el, offset: 0 }; } }; // Transform an offset into a range that we can use to restore the cursor var getRangeFromOffset = function (element) { var range = { start: { el: null, offset: 0 }, end: { el: null, offset: 0 } }; ['start', 'end'].forEach(function (t) { var offset = offsetRange[t]; var res = getFinalRange(element, offset); range[t].el = res.el; range[t].offset = res.offset; }); return range; }; cursor.getNewOffset = function (ops) { return { selectionStart: offsetTransformRange(offsetRange.start, ops), selectionEnd: offsetTransformRange(offsetRange.end, ops) }; }; cursor.getNewRange = function (data, ops) { offsetRange.start = offsetTransformRange(data.start, ops); offsetRange.end = offsetTransformRange(data.end, ops); var range = getRangeFromOffset(inner); return range; }; // Restore the cursor position after applying the changes. cursor.restoreOffset = function (ops) { try { offsetRange.start = offsetTransformRange(offsetRange.start, ops); offsetRange.end = offsetTransformRange(offsetRange.end, ops); var range = getRangeFromOffset(inner); var sel = cursor.makeSelection(); var r = cursor.makeRange(); cursor.fixStart(range.start.el, range.start.offset); cursor.fixEnd(range.end.el, range.end.offset); cursor.fixSelection(sel, r); cursor.brFix(); } catch (e) { console.error(e); } }; // there ought to only be one cursor at a time, so let's just // keep it internally var Range = cursor.Range = { start: { el: null, offset: 0 }, end: { el: null, offset:0 } }; /* cursor.update takes notes about wherever the cursor was last seen in the event of a cursor loss, the information produced by side effects of this function should be used to recover the cursor returns an error string if no range is found */ cursor.update = function (sel, root) { verbose("cursor.update"); root = root || inner; sel = sel || Rangy.getSelection(root); // if the root element has no focus, there will be no range if (!sel.rangeCount) { return; } var range = sel.getRangeAt(0); // Big R Range is caught in closure, and maintains persistent state ['start', 'end'].forEach(function (pos) { Range[pos].el = range[pos+'Container']; Range[pos].offset = range[pos+'Offset']; }); }; cursor.exists = function () { return (Range.start.el?1:0) | (Range.end.el?2:0); }; /* 0 if neither 1 if start 2 if end 3 if start and end */ cursor.inNode = function (el) { var state = ['start', 'end'].map(function (pos, i) { return Tree.contains(el, Range[pos].el)? i +1: 0; }); return state[0] | state[1]; }; var confineOffsetToElement = cursor.confineOffsetToElement = function (el, offset) { return Math.max(Math.min(offset, el.textContent.length), 0); }; var makeSelection = cursor.makeSelection = function () { var sel = Rangy.getSelection(inner); return sel; }; var makeRange = cursor.makeRange = function () { return Rangy.createRange(); }; var fixStart = cursor.fixStart = function (el, offset) { Range.start.el = el; Range.start.offset = confineOffsetToElement(el, (typeof offset !== 'undefined') ? offset : Range.start.offset); }; var fixEnd = cursor.fixEnd = function (el, offset) { Range.end.el = el; Range.end.offset = confineOffsetToElement(el, (typeof offset !== 'undefined') ? offset : Range.end.offset); }; var fixSelection = cursor.fixSelection = function (sel, range) { try { if (Tree.contains(Range.start.el, inner) && Tree.contains(Range.end.el, inner)) { var order = Tree.orderOfNodes(Range.start.el, Range.end.el, inner); var backward; // this could all be one line but nobody would be able to read it if (order === -1) { // definitely backward backward = true; } else if (order === 0) { // might be backward, check offsets to know for sure backward = (Range.start.offset > Range.end.offset); } else { // definitely not backward backward = false; } if (backward) { range.setStart(Range.end.el, Range.end.offset); range.setEnd(Range.start.el, Range.start.offset); } else { range.setStart(Range.start.el, Range.start.offset); range.setEnd(Range.end.el, Range.end.offset); } // actually set the cursor to the new range sel.setSingleRange(range); } else { var errText = "[cursor.fixSelection] At least one of the " + "cursor nodes did not exist, could not fix selection"; //console.error(errText); return errText; } } catch (e) { console.error(e); } }; cursor.pushDelta = function (oldVal, newVal) { if (oldVal === newVal) { return; } var commonStart = 0; while (oldVal.charAt(commonStart) === newVal.charAt(commonStart)) { commonStart++; } var commonEnd = 0; while (oldVal.charAt(oldVal.length - 1 - commonEnd) === newVal.charAt(newVal.length - 1 - commonEnd) && commonEnd + commonStart < oldVal.length && commonEnd + commonStart < newVal.length) { commonEnd++; } var insert = false, remove = false; if (oldVal.length !== commonStart + commonEnd) { // there was a removal? remove = true; } if (newVal.length !== commonStart + commonEnd) { // there was an insertion? insert = true; } var lengthDelta = newVal.length - oldVal.length; return { commonStart: commonStart, commonEnd: commonEnd, delta: lengthDelta, insert: insert, remove: remove }; }; cursor.transformRange = function (cursorRange, ops) { var transformCursor = function (cursor, op) { if (!op) { return cursor; } var pos = op.offset; var remove = op.toRemove; var insert = op.toInsert.length; if (typeof cursor === 'undefined') { return; } if (typeof remove === 'number' && pos < cursor) { cursor -= Math.min(remove, cursor - pos); } if (typeof insert === 'number' && pos < cursor) { cursor += insert; } return cursor; }; var c = cursorRange.offset; if (Array.isArray(ops)) { for (var i = ops.length - 1; i >= 0; i--) { c = transformCursor(c, ops[i]); } cursorRange.offset = c; } }; cursor.brFix = function () { cursor.update(); var start = Range.start; var end = Range.end; if (!start.el) { return; } if (start.el === end.el && start.offset === end.offset) { if (start.el.tagName === 'BR') { var br = start.el; var P = (Tree.indexOfNode(br) === 0 ? br.parentNode: br.previousSibling); [cursor.fixStart, cursor.fixEnd].forEach(function (f) { f(P, 0); }); cursor.fixSelection(cursor.makeSelection(), cursor.makeRange()); } } }; cursor.lastTextNode = function () { var lastEl = Tree.rightmostNode(inner); if (lastEl && lastEl.nodeType === 3) { return lastEl; } var firstEl = Tree.leftmostNode(inner); while (lastEl !== firstEl) { lastEl = Tree.previousNode(lastEl, inner); if (lastEl && lastEl.nodeType === 3) { return lastEl; } } return lastEl; }; cursor.firstTextNode = function () { var firstEl = Tree.leftmostNode(inner); if (firstEl && firstEl.nodeType === 3) { return firstEl; } var lastEl = Tree.rightmostNode(inner); while (firstEl !== lastEl) { firstEl = Tree.nextNode(firstEl, inner); if (firstEl && firstEl.nodeType === 3) { return firstEl; } } return firstEl; }; cursor.setToStart = function () { var el = cursor.firstTextNode(); if (!el) { return; } fixStart(el, 0); fixEnd(el, 0); fixSelection(makeSelection(), makeRange()); return el; }; cursor.setToEnd = function () { var el = cursor.lastTextNode(); if (!el) { return; } var offset = el.textContent.length; fixStart(el, offset); fixEnd(el, offset); fixSelection(makeSelection(), makeRange()); return el; }; return cursor; }; Cursor.Tree = Tree; return Cursor; });