put zero-width spaces in caret nodes so chrome doesn't ignore them

this requires an update of the editor DOM > text & caret offset logic,
as the ZWS need to be ignored.
pull/21833/head
Bruno Windels 2019-06-20 14:44:18 +02:00
parent b16bc0178a
commit 366a4aa308
2 changed files with 68 additions and 23 deletions

View File

@ -15,6 +15,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {ZERO_WIDTH_SPACE, isCaretNode} from "./render";
export function walkDOMDepthFirst(rootNode, enterNodeCallback, leaveNodeCallback) { export function walkDOMDepthFirst(rootNode, enterNodeCallback, leaveNodeCallback) {
let node = rootNode.firstChild; let node = rootNode.firstChild;
while (node && node !== rootNode) { while (node && node !== rootNode) {
@ -38,27 +40,54 @@ export function walkDOMDepthFirst(rootNode, enterNodeCallback, leaveNodeCallback
} }
export function getCaretOffsetAndText(editor, sel) { export function getCaretOffsetAndText(editor, sel) {
let {focusNode} = sel; let {focusNode, focusOffset} = sel;
const {focusOffset} = sel; // sometimes focusNode is an element, and then focusOffset means
let caretOffset = focusOffset; // the index of a child element ... - 1 🤷
if (focusNode.nodeType === Node.ELEMENT_NODE && focusOffset !== 0) {
focusNode = focusNode.childNodes[focusOffset - 1];
focusOffset = focusNode.textContent.length;
}
const {text, focusNodeOffset} = getTextAndFocusNodeOffset(editor, focusNode, focusOffset);
const caret = getCaret(focusNode, focusNodeOffset, focusOffset);
return {caret, text};
}
// gets the caret position details, ignoring and adjusting to
// the ZWS if you're typing in a caret node
function getCaret(focusNode, focusNodeOffset, focusOffset) {
let atNodeEnd = focusOffset === focusNode.textContent.length;
if (focusNode.nodeType === Node.TEXT_NODE && isCaretNode(focusNode.parentElement)) {
const zwsIdx = focusNode.nodeValue.indexOf(ZERO_WIDTH_SPACE);
if (zwsIdx !== -1 && zwsIdx < focusOffset) {
focusOffset -= 1;
}
// if typing in a caret node, you're either typing before or after the ZWS.
// In both cases, you should be considered at node end because the ZWS is
// not included in the text here, and once the model is updated and rerendered,
// that caret node will be removed.
atNodeEnd = true;
}
return {offset: focusNodeOffset + focusOffset, atNodeEnd};
}
// gets the text of the editor as a string,
// and the offset in characters where the focusNode starts in that string
// all ZWS from caret nodes are filtered out
function getTextAndFocusNodeOffset(editor, focusNode, focusOffset) {
let focusNodeOffset = 0;
let foundCaret = false; let foundCaret = false;
let text = ""; let text = "";
if (focusNode.nodeType === Node.ELEMENT_NODE && focusOffset !== 0) {
focusNode = focusNode.childNodes[focusOffset - 1];
caretOffset = focusNode.textContent.length;
}
function enterNodeCallback(node) { function enterNodeCallback(node) {
const nodeText = node.nodeType === Node.TEXT_NODE && node.nodeValue;
if (!foundCaret) { if (!foundCaret) {
if (node === focusNode) { if (node === focusNode) {
foundCaret = true; foundCaret = true;
} }
} }
const nodeText = node.nodeType === Node.TEXT_NODE && getTextNodeValue(node);
if (nodeText) { if (nodeText) {
if (!foundCaret) { if (!foundCaret) {
caretOffset += nodeText.length; focusNodeOffset += nodeText.length;
} }
text += nodeText; text += nodeText;
} }
@ -73,14 +102,30 @@ export function getCaretOffsetAndText(editor, sel) {
if (node.tagName === "DIV" && node.nextSibling && node.nextSibling.tagName === "DIV") { if (node.tagName === "DIV" && node.nextSibling && node.nextSibling.tagName === "DIV") {
text += "\n"; text += "\n";
if (!foundCaret) { if (!foundCaret) {
caretOffset += 1; focusNodeOffset += 1;
} }
} }
} }
walkDOMDepthFirst(editor, enterNodeCallback, leaveNodeCallback); walkDOMDepthFirst(editor, enterNodeCallback, leaveNodeCallback);
const atNodeEnd = sel.focusOffset === sel.focusNode.textContent.length; return {text, focusNodeOffset};
const caret = {atNodeEnd, offset: caretOffset}; }
return {caret, text};
// get text value of text node, ignoring ZWS if it's a caret node
function getTextNodeValue(node) {
const nodeText = node.nodeValue;
// filter out ZWS for caret nodes
if (isCaretNode(node.parentElement)) {
// typed in the caret node, so there is now something more in it than the ZWS
// so filter out the ZWS, and take the typed text into account
if (nodeText.length !== 1) {
return nodeText.replace(ZERO_WIDTH_SPACE, "");
} else {
// only contains ZWS, which is ignored, so return emtpy string
return "";
}
} else {
return nodeText;
}
} }

View File

@ -33,25 +33,25 @@ function insertAfter(node, nodeToInsert) {
} }
} }
// a caret node is an empty node that allows the caret to be place export const ZERO_WIDTH_SPACE = "\u200b";
// a caret node is a node that allows the caret to be placed
// where otherwise it wouldn't be possible // where otherwise it wouldn't be possible
// (e.g. next to a pill span without adjacent text node) // (e.g. next to a pill span without adjacent text node)
function createCaretNode() { function createCaretNode() {
const span = document.createElement("span"); const span = document.createElement("span");
span.className = "caret"; span.className = "caret";
span.appendChild(document.createTextNode(ZERO_WIDTH_SPACE));
return span; return span;
} }
function updateCaretNode(node) { function updateCaretNode(node) {
// ensure the caret node is empty // ensure the caret node contains only a zero-width space
// otherwise they'll break everything if (node.textContent !== ZERO_WIDTH_SPACE) {
// as only things part of the model should have text in them node.textContent = ZERO_WIDTH_SPACE;
// browsers could end up typing in the caret node for any }
// number of reasons, so revert this.
node.textContent = "";
} }
function isCaretNode(node) { export function isCaretNode(node) {
return node && node.tagName === "SPAN" && node.className === "caret"; return node && node.tagName === "SPAN" && node.className === "caret";
} }
@ -86,8 +86,8 @@ function reconcileLine(lineContainer, parts) {
if (needsCaretNodeBefore(part, prevPart)) { if (needsCaretNodeBefore(part, prevPart)) {
if (isCaretNode(currentNode)) { if (isCaretNode(currentNode)) {
currentNode = currentNode.nextSibling;
updateCaretNode(currentNode); updateCaretNode(currentNode);
currentNode = currentNode.nextSibling;
} else { } else {
lineContainer.insertBefore(createCaretNode(), currentNode); lineContainer.insertBefore(createCaretNode(), currentNode);
} }