From dbad925e5e63ae5661edce65b71f0412511f2de6 Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 26 Nov 2018 16:23:16 +0100 Subject: [PATCH] New cursor recovery system (beta) --- www/common/cursor.js | 225 +++++++++++++++++++++++++++++++++++++++++++ www/pad/inner.js | 21 ++-- 2 files changed, 240 insertions(+), 6 deletions(-) diff --git a/www/common/cursor.js b/www/common/cursor.js index b19c02850..fffbe1db6 100644 --- a/www/common/cursor.js +++ b/www/common/cursor.js @@ -8,6 +8,231 @@ define([ 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 ( ==> 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 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'] + }; + var current = inNode.el; + var path = []; + while (current !== element) { + path.unshift(current); + current = current.parentElement; + } + + 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 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
, 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++) { + newOffset -= (getTextNodeValue(el.childNodes[i]) || el.childNodes[i].outerHTML).length; + 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; + }; + + // 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 = { diff --git a/www/pad/inner.js b/www/pad/inner.js index 37038f7db..ff7fe47b5 100644 --- a/www/pad/inner.js +++ b/www/pad/inner.js @@ -306,6 +306,7 @@ define([ return true; } +/* cursor.update(); // no use trying to recover the cursor if it doesn't exist @@ -314,6 +315,7 @@ define([ /* frame is either 0, 1, 2, or 3, depending on which cursor frames were affected: none, first, last, or both */ +/* var frame = info.frame = cursor.inNode(info.node); if (!frame) { return; } @@ -325,19 +327,15 @@ define([ if (frame & 1) { // push cursor start if necessary cursor.transformRange(cursor.Range.start, ops); - /*if (pushes.commonStart < cursor.Range.start.offset) { - cursor.Range.start.offset += pushes.delta; - }*/ } if (frame & 2) { // push cursor end if necessary cursor.transformRange(cursor.Range.end, ops); - /*if (pushes.commonStart < cursor.Range.end.offset) { - cursor.Range.end.offset += pushes.delta; - }*/ } } +*/ }, +/* postDiffApply: function (info) { if (info.frame) { if (info.node) { @@ -351,6 +349,7 @@ define([ cursor.fixSelection(sel, range); } } +*/ }; }; @@ -515,9 +514,19 @@ define([ $(el).remove(); }); + // Get cursor position + cursor.offsetUpdate(); + var oldText = inner.outerHTML; + + // Apply the changes var patch = (DD).diff(inner, userDocStateDom); (DD).apply(inner, patch); + // Restore cursor position + var newText = inner.outerHTML; + var ops = ChainPad.Diff.diff(oldText, newText); + cursor.restoreOffset(ops); + // MEDIATAG: Migrate old mediatags to the widget system $(inner).find('media-tag:not(.cke_widget_element)').each(function (i, el) { var element = new window.CKEDITOR.dom.element(el);