Merge pull request #3126 from matrix-org/bwindels/caret-refactoring
Editor caret improvements
This commit is contained in:
commit
1c7af38d83
5 changed files with 300 additions and 104 deletions
|
@ -78,6 +78,14 @@ export default class MessageEditor extends React.Component {
|
||||||
this.model.update(text, event.inputType, caret);
|
this.model.update(text, event.inputType, caret);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_insertText(textToInsert, inputType = "insertText") {
|
||||||
|
const sel = document.getSelection();
|
||||||
|
const {caret, text} = getCaretOffsetAndText(this._editorRef, sel);
|
||||||
|
const newText = text.substr(0, caret.offset) + textToInsert + text.substr(caret.offset);
|
||||||
|
caret.offset += textToInsert.length;
|
||||||
|
this.model.update(newText, inputType, caret);
|
||||||
|
}
|
||||||
|
|
||||||
_isCaretAtStart() {
|
_isCaretAtStart() {
|
||||||
const {caret} = getCaretOffsetAndText(this._editorRef, document.getSelection());
|
const {caret} = getCaretOffsetAndText(this._editorRef, document.getSelection());
|
||||||
return caret.offset === 0;
|
return caret.offset === 0;
|
||||||
|
@ -92,7 +100,7 @@ export default class MessageEditor extends React.Component {
|
||||||
// insert newline on Shift+Enter
|
// insert newline on Shift+Enter
|
||||||
if (event.shiftKey && event.key === "Enter") {
|
if (event.shiftKey && event.key === "Enter") {
|
||||||
event.preventDefault(); // just in case the browser does support this
|
event.preventDefault(); // just in case the browser does support this
|
||||||
document.execCommand("insertHTML", undefined, "\n");
|
this._insertText("\n");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// autocomplete or enter to send below shouldn't have any modifier keys pressed.
|
// autocomplete or enter to send below shouldn't have any modifier keys pressed.
|
||||||
|
|
|
@ -15,50 +15,104 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import {needsCaretNodeBefore, needsCaretNodeAfter} from "./render";
|
||||||
|
|
||||||
export function setCaretPosition(editor, model, caretPosition) {
|
export function setCaretPosition(editor, model, caretPosition) {
|
||||||
const sel = document.getSelection();
|
const sel = document.getSelection();
|
||||||
sel.removeAllRanges();
|
sel.removeAllRanges();
|
||||||
const range = document.createRange();
|
const range = document.createRange();
|
||||||
|
const {offset, lineIndex, nodeIndex} = getLineAndNodePosition(model, caretPosition);
|
||||||
|
const lineNode = editor.childNodes[lineIndex];
|
||||||
|
|
||||||
|
let focusNode;
|
||||||
|
// empty line with just a <br>
|
||||||
|
if (nodeIndex === -1) {
|
||||||
|
focusNode = lineNode;
|
||||||
|
} else {
|
||||||
|
focusNode = lineNode.childNodes[nodeIndex];
|
||||||
|
// make sure we have a text node
|
||||||
|
if (focusNode.nodeType === Node.ELEMENT_NODE && focusNode.firstChild) {
|
||||||
|
focusNode = focusNode.firstChild;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
range.setStart(focusNode, offset);
|
||||||
|
range.collapse(true);
|
||||||
|
sel.addRange(range);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLineAndNodePosition(model, caretPosition) {
|
||||||
const {parts} = model;
|
const {parts} = model;
|
||||||
const {index} = caretPosition;
|
const partIndex = caretPosition.index;
|
||||||
|
const lineResult = findNodeInLineForPart(parts, partIndex);
|
||||||
|
const {lineIndex} = lineResult;
|
||||||
|
let {nodeIndex} = lineResult;
|
||||||
let {offset} = caretPosition;
|
let {offset} = caretPosition;
|
||||||
|
// we're at an empty line between a newline part
|
||||||
|
// and another newline part or end/start of parts.
|
||||||
|
// set offset to 0 so it gets set to the <br> inside the line container
|
||||||
|
if (nodeIndex === -1) {
|
||||||
|
offset = 0;
|
||||||
|
} else {
|
||||||
|
// move caret out of uneditable part (into caret node, or empty line br) if needed
|
||||||
|
({nodeIndex, offset} = moveOutOfUneditablePart(parts, partIndex, nodeIndex, offset));
|
||||||
|
}
|
||||||
|
return {lineIndex, nodeIndex, offset};
|
||||||
|
}
|
||||||
|
|
||||||
|
function findNodeInLineForPart(parts, partIndex) {
|
||||||
let lineIndex = 0;
|
let lineIndex = 0;
|
||||||
let nodeIndex = -1;
|
let nodeIndex = -1;
|
||||||
for (let i = 0; i <= index; ++i) {
|
|
||||||
|
let prevPart = null;
|
||||||
|
// go through to parts up till (and including) the index
|
||||||
|
// to find newline parts
|
||||||
|
for (let i = 0; i <= partIndex; ++i) {
|
||||||
const part = parts[i];
|
const part = parts[i];
|
||||||
if (part && part.type === "newline") {
|
if (part.type === "newline") {
|
||||||
if (i < index) {
|
lineIndex += 1;
|
||||||
lineIndex += 1;
|
nodeIndex = -1;
|
||||||
nodeIndex = -1;
|
prevPart = null;
|
||||||
} else {
|
} else {
|
||||||
// if index points at a newline part,
|
nodeIndex += 1;
|
||||||
// put the caret at the end of the previous part
|
if (needsCaretNodeBefore(part, prevPart)) {
|
||||||
// so it stays on the same line
|
nodeIndex += 1;
|
||||||
const prevPart = parts[i - 1];
|
}
|
||||||
offset = prevPart ? prevPart.text.length : 0;
|
// only jump over caret node if we're not at our destination node already,
|
||||||
|
// as we'll assume in moveOutOfUneditablePart that nodeIndex
|
||||||
|
// refers to the node corresponding to the part,
|
||||||
|
// and not an adjacent caret node
|
||||||
|
if (i < partIndex) {
|
||||||
|
const nextPart = parts[i + 1];
|
||||||
|
const isLastOfLine = !nextPart || nextPart.type === "newline";
|
||||||
|
if (needsCaretNodeAfter(part, isLastOfLine)) {
|
||||||
|
nodeIndex += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prevPart = part;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {lineIndex, nodeIndex};
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveOutOfUneditablePart(parts, partIndex, nodeIndex, offset) {
|
||||||
|
// move caret before or after uneditable part
|
||||||
|
const part = parts[partIndex];
|
||||||
|
if (part && !part.canEdit) {
|
||||||
|
if (offset === 0) {
|
||||||
|
nodeIndex -= 1;
|
||||||
|
const prevPart = parts[partIndex - 1];
|
||||||
|
// if the previous node is a caret node, it's empty
|
||||||
|
// so the offset can stay at 0
|
||||||
|
// only when it's not, we need to set the offset
|
||||||
|
// at the end of the node
|
||||||
|
if (!needsCaretNodeBefore(part, prevPart)) {
|
||||||
|
offset = prevPart.text.length;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
nodeIndex += 1;
|
nodeIndex += 1;
|
||||||
|
offset = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let focusNode;
|
return {nodeIndex, offset};
|
||||||
const lineNode = editor.childNodes[lineIndex];
|
|
||||||
if (lineNode) {
|
|
||||||
focusNode = lineNode.childNodes[nodeIndex];
|
|
||||||
if (!focusNode) {
|
|
||||||
focusNode = lineNode;
|
|
||||||
} else if (focusNode.nodeType === Node.ELEMENT_NODE) {
|
|
||||||
focusNode = focusNode.childNodes[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// node not found, set caret at end
|
|
||||||
if (!focusNode) {
|
|
||||||
range.selectNodeContents(editor);
|
|
||||||
range.collapse(false);
|
|
||||||
} else {
|
|
||||||
// make sure we have a text node
|
|
||||||
range.setStart(focusNode, offset);
|
|
||||||
range.collapse(true);
|
|
||||||
}
|
|
||||||
sel.addRange(range);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,8 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import {CARET_NODE_CHAR, 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(CARET_NODE_CHAR);
|
||||||
|
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(CARET_NODE_CHAR, "");
|
||||||
|
} else {
|
||||||
|
// only contains ZWS, which is ignored, so return emtpy string
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nodeText;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -103,8 +103,7 @@ export default class EditorModel {
|
||||||
}
|
}
|
||||||
this._mergeAdjacentParts();
|
this._mergeAdjacentParts();
|
||||||
const caretOffset = diff.at - removedOffsetDecrease + addedLen;
|
const caretOffset = diff.at - removedOffsetDecrease + addedLen;
|
||||||
let newPosition = this.positionForOffset(caretOffset, true);
|
const newPosition = this.positionForOffset(caretOffset, true);
|
||||||
newPosition = newPosition.skipUneditableParts(this._parts);
|
|
||||||
this._setActivePart(newPosition);
|
this._setActivePart(newPosition);
|
||||||
this._updateCallback(newPosition);
|
this._updateCallback(newPosition);
|
||||||
}
|
}
|
||||||
|
@ -140,10 +139,9 @@ export default class EditorModel {
|
||||||
let pos;
|
let pos;
|
||||||
if (replacePart) {
|
if (replacePart) {
|
||||||
this._replacePart(this._autoCompletePartIdx, replacePart);
|
this._replacePart(this._autoCompletePartIdx, replacePart);
|
||||||
let index = this._autoCompletePartIdx;
|
const index = this._autoCompletePartIdx;
|
||||||
if (caretOffset === undefined) {
|
if (caretOffset === undefined) {
|
||||||
caretOffset = 0;
|
caretOffset = replacePart.text.length;
|
||||||
index += 1;
|
|
||||||
}
|
}
|
||||||
pos = new DocumentPosition(index, caretOffset);
|
pos = new DocumentPosition(index, caretOffset);
|
||||||
}
|
}
|
||||||
|
@ -158,11 +156,11 @@ export default class EditorModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
_mergeAdjacentParts(docPos) {
|
_mergeAdjacentParts(docPos) {
|
||||||
let prevPart = this._parts[0];
|
let prevPart;
|
||||||
for (let i = 1; i < this._parts.length; ++i) {
|
for (let i = 0; i < this._parts.length; ++i) {
|
||||||
let part = this._parts[i];
|
let part = this._parts[i];
|
||||||
const isEmpty = !part.text.length;
|
const isEmpty = !part.text.length;
|
||||||
const isMerged = !isEmpty && prevPart.merge(part);
|
const isMerged = !isEmpty && prevPart && prevPart.merge(part);
|
||||||
if (isEmpty || isMerged) {
|
if (isEmpty || isMerged) {
|
||||||
// remove empty or merged part
|
// remove empty or merged part
|
||||||
part = prevPart;
|
part = prevPart;
|
||||||
|
@ -283,13 +281,4 @@ class DocumentPosition {
|
||||||
get offset() {
|
get offset() {
|
||||||
return this._offset;
|
return this._offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
skipUneditableParts(parts) {
|
|
||||||
const part = parts[this.index];
|
|
||||||
if (part && !part.canEdit) {
|
|
||||||
return new DocumentPosition(this.index + 1, 0);
|
|
||||||
} else {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,137 @@ 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a BOM marker for caret nodes.
|
||||||
|
// On a first test, they seem to be filtered out when copying text out of the editor,
|
||||||
|
// but this could be platform dependent.
|
||||||
|
// As a precautionary measure, I chose the character that slate also uses.
|
||||||
|
export const CARET_NODE_CHAR = "\ufeff";
|
||||||
|
// a caret node is a node that allows the caret to be placed
|
||||||
|
// 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 = "caretNode";
|
||||||
|
span.appendChild(document.createTextNode(CARET_NODE_CHAR));
|
||||||
|
return span;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCaretNode(node) {
|
||||||
|
// ensure the caret node contains only a zero-width space
|
||||||
|
if (node.textContent !== CARET_NODE_CHAR) {
|
||||||
|
node.textContent = CARET_NODE_CHAR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isCaretNode(node) {
|
||||||
|
return node && node.tagName === "SPAN" && node.className === "caretNode";
|
||||||
|
}
|
||||||
|
|
||||||
|
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)) {
|
||||||
|
updateCaretNode(currentNode);
|
||||||
|
currentNode = currentNode.nextSibling;
|
||||||
|
} 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 +156,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 +170,14 @@ export function renderModel(editor, model) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parts.length) {
|
if (parts.length) {
|
||||||
parts.forEach((part, j) => {
|
reconcileLine(lineContainer, parts);
|
||||||
let partNode = lineContainer.childNodes[j];
|
|
||||||
while (partNode && !part.canUpdateDOMNode(partNode)) {
|
|
||||||
lineContainer.removeChild(partNode);
|
|
||||||
partNode = lineContainer.childNodes[j];
|
|
||||||
}
|
|
||||||
if (partNode && part) {
|
|
||||||
part.updateDOMNode(partNode);
|
|
||||||
} else if (part) {
|
|
||||||
lineContainer.appendChild(part.toDOMNode());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let surplusElementCount = Math.max(0, lineContainer.childNodes.length - parts.length);
|
|
||||||
while (surplusElementCount) {
|
|
||||||
lineContainer.removeChild(lineContainer.lastChild);
|
|
||||||
--surplusElementCount;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// empty div needs to have a BR in it to give it height
|
reconcileEmptyLine(lineContainer);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
if (lines.length) {
|
||||||
|
removeNextSiblings(editor.children[lines.length]);
|
||||||
|
} else {
|
||||||
|
removeChildren(editor);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue