insert "caret nodes" where pills don't have an adjacent text node
just empty spans, where the caret can be placed.
This commit is contained in:
parent
9591e6b0d3
commit
75fc769742
1 changed files with 136 additions and 40 deletions
|
@ -15,6 +15,133 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export function needsCaretNodeBefore(part, prevPart) {
|
||||||
|
const isFirst = !prevPart || prevPart.type === "newline";
|
||||||
|
return !part.canEdit && (isFirst || !prevPart.canEdit);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function needsCaretNodeAfter(part, isLastOfLine) {
|
||||||
|
return !part.canEdit && isLastOfLine;
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertAfter(node, nodeToInsert) {
|
||||||
|
const next = node.nextSibling;
|
||||||
|
if (next) {
|
||||||
|
node.parentElement.insertBefore(nodeToInsert, next);
|
||||||
|
} else {
|
||||||
|
node.parentElement.appendChild(nodeToInsert);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// a caret node is an empty node that allows the caret to be place
|
||||||
|
// where otherwise it wouldn't be possible
|
||||||
|
// (e.g. next to a pill span without adjacent text node)
|
||||||
|
function createCaretNode() {
|
||||||
|
const span = document.createElement("span");
|
||||||
|
span.className = "caret";
|
||||||
|
return span;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCaretNode(node) {
|
||||||
|
// ensure the caret node is empty
|
||||||
|
// otherwise they'll break everything
|
||||||
|
// as only things part of the model should have text in them
|
||||||
|
// browsers could end up typing in the caret node for any
|
||||||
|
// number of reasons, so revert this.
|
||||||
|
node.textContent = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCaretNode(node) {
|
||||||
|
return node && node.tagName === "SPAN" && node.className === "caret";
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeNextSiblings(node) {
|
||||||
|
if (!node) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
node = node.nextSibling;
|
||||||
|
while (node) {
|
||||||
|
const removeNode = node;
|
||||||
|
node = node.nextSibling;
|
||||||
|
removeNode.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeChildren(parent) {
|
||||||
|
const firstChild = parent.firstChild;
|
||||||
|
if (firstChild) {
|
||||||
|
removeNextSiblings(firstChild);
|
||||||
|
firstChild.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function reconcileLine(lineContainer, parts) {
|
||||||
|
let currentNode;
|
||||||
|
let prevPart;
|
||||||
|
const lastPart = parts[parts.length - 1];
|
||||||
|
|
||||||
|
for (const part of parts) {
|
||||||
|
const isFirst = !prevPart;
|
||||||
|
currentNode = isFirst ? lineContainer.firstChild : currentNode.nextSibling;
|
||||||
|
|
||||||
|
if (needsCaretNodeBefore(part, prevPart)) {
|
||||||
|
if (isCaretNode(currentNode)) {
|
||||||
|
currentNode = currentNode.nextSibling;
|
||||||
|
updateCaretNode(currentNode);
|
||||||
|
} else {
|
||||||
|
lineContainer.insertBefore(createCaretNode(), currentNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// remove nodes until matching current part
|
||||||
|
while (currentNode && !part.canUpdateDOMNode(currentNode)) {
|
||||||
|
const nextNode = currentNode.nextSibling;
|
||||||
|
lineContainer.removeChild(currentNode);
|
||||||
|
currentNode = nextNode;
|
||||||
|
}
|
||||||
|
// update or insert node for current part
|
||||||
|
if (currentNode && part) {
|
||||||
|
part.updateDOMNode(currentNode);
|
||||||
|
} else if (part) {
|
||||||
|
currentNode = part.toDOMNode();
|
||||||
|
// hooks up nextSibling for next iteration
|
||||||
|
lineContainer.appendChild(currentNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needsCaretNodeAfter(part, part === lastPart)) {
|
||||||
|
if (isCaretNode(currentNode.nextSibling)) {
|
||||||
|
currentNode = currentNode.nextSibling;
|
||||||
|
updateCaretNode(currentNode);
|
||||||
|
} else {
|
||||||
|
const caretNode = createCaretNode();
|
||||||
|
insertAfter(currentNode, caretNode);
|
||||||
|
currentNode = caretNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prevPart = part;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeNextSiblings(currentNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
function reconcileEmptyLine(lineContainer) {
|
||||||
|
// empty div needs to have a BR in it to give it height
|
||||||
|
let foundBR = false;
|
||||||
|
let partNode = lineContainer.firstChild;
|
||||||
|
while (partNode) {
|
||||||
|
const nextNode = partNode.nextSibling;
|
||||||
|
if (!foundBR && partNode.tagName === "BR") {
|
||||||
|
foundBR = true;
|
||||||
|
} else {
|
||||||
|
partNode.remove();
|
||||||
|
}
|
||||||
|
partNode = nextNode;
|
||||||
|
}
|
||||||
|
if (!foundBR) {
|
||||||
|
lineContainer.appendChild(document.createElement("br"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function renderModel(editor, model) {
|
export function renderModel(editor, model) {
|
||||||
const lines = model.parts.reduce((lines, part) => {
|
const lines = model.parts.reduce((lines, part) => {
|
||||||
if (part.type === "newline") {
|
if (part.type === "newline") {
|
||||||
|
@ -25,8 +152,9 @@ export function renderModel(editor, model) {
|
||||||
}
|
}
|
||||||
return lines;
|
return lines;
|
||||||
}, [[]]);
|
}, [[]]);
|
||||||
// TODO: refactor this code, DRY it
|
|
||||||
lines.forEach((parts, i) => {
|
lines.forEach((parts, i) => {
|
||||||
|
// find first (and remove anything else) div without className
|
||||||
|
// (as browsers insert these in contenteditable) line container
|
||||||
let lineContainer = editor.childNodes[i];
|
let lineContainer = editor.childNodes[i];
|
||||||
while (lineContainer && (lineContainer.tagName !== "DIV" || !!lineContainer.className)) {
|
while (lineContainer && (lineContainer.tagName !== "DIV" || !!lineContainer.className)) {
|
||||||
editor.removeChild(lineContainer);
|
editor.removeChild(lineContainer);
|
||||||
|
@ -38,46 +166,14 @@ export function renderModel(editor, model) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parts.length) {
|
if (parts.length) {
|
||||||
parts.forEach((part, j) => {
|
reconcileLine(lineContainer, parts);
|
||||||
let partNode = lineContainer.childNodes[j];
|
} else {
|
||||||
while (partNode && !part.canUpdateDOMNode(partNode)) {
|
reconcileEmptyLine(lineContainer);
|
||||||
lineContainer.removeChild(partNode);
|
|
||||||
partNode = lineContainer.childNodes[j];
|
|
||||||
}
|
|
||||||
if (partNode && part) {
|
|
||||||
part.updateDOMNode(partNode);
|
|
||||||
} else if (part) {
|
|
||||||
lineContainer.appendChild(part.toDOMNode());
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
if (lines.length) {
|
||||||
let surplusElementCount = Math.max(0, lineContainer.childNodes.length - parts.length);
|
removeNextSiblings(editor.children[lines.length]);
|
||||||
while (surplusElementCount) {
|
|
||||||
lineContainer.removeChild(lineContainer.lastChild);
|
|
||||||
--surplusElementCount;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// empty div needs to have a BR in it to give it height
|
removeChildren(editor);
|
||||||
let foundBR = false;
|
|
||||||
let partNode = lineContainer.firstChild;
|
|
||||||
while (partNode) {
|
|
||||||
const nextNode = partNode.nextSibling;
|
|
||||||
if (!foundBR && partNode.tagName === "BR") {
|
|
||||||
foundBR = true;
|
|
||||||
} else {
|
|
||||||
lineContainer.removeChild(partNode);
|
|
||||||
}
|
|
||||||
partNode = nextNode;
|
|
||||||
}
|
|
||||||
if (!foundBR) {
|
|
||||||
lineContainer.appendChild(document.createElement("br"));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let surplusElementCount = Math.max(0, editor.childNodes.length - lines.length);
|
|
||||||
while (surplusElementCount) {
|
|
||||||
editor.removeChild(editor.lastChild);
|
|
||||||
--surplusElementCount;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue