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.
This commit is contained in:
parent
b16bc0178a
commit
366a4aa308
2 changed files with 68 additions and 23 deletions
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue