Squire: improved handling of BR elements

This commit is contained in:
the-djmaze 2024-10-01 17:56:43 +02:00
parent f43c2c92b5
commit 584728bc89
2 changed files with 191 additions and 255 deletions

View file

@ -56,10 +56,11 @@
.squire-wysiwyg {
font-family: var(--fontSans);
/* Prevent collapse of empty elements so that the caret can be placed in there */
/* Prevent collapse of empty elements so that the caret can be placed in there
*:empty::before {
content: "\200B";
}
*/
ul {
padding-left: 40px;

View file

@ -349,203 +349,6 @@
return range;
};
// source/node/MergeSplit.ts
var fixCursor = (node) => {
let fixer = null;
if (node instanceof Text) {
return node;
}
if (isInline(node)) {
let child = node.firstChild;
if (cantFocusEmptyTextNodes) {
while (child && child instanceof Text && !child.data) {
node.removeChild(child);
child = node.firstChild;
}
}
if (!child) {
if (cantFocusEmptyTextNodes) {
fixer = document.createTextNode(ZWS);
} else {
fixer = document.createTextNode("");
}
}
} else if ((node instanceof Element || node instanceof DocumentFragment) && !node.querySelector("BR")) {
fixer = createElement("BR");
let parent = node;
let child;
while ((child = parent.lastElementChild) && !isInline(child)) {
parent = child;
}
node = parent;
}
if (fixer) {
try {
node.appendChild(fixer);
} catch (error) {
}
}
return node;
};
var fixContainer = (container, root) => {
let wrapper = null;
[...container.childNodes].forEach((child) => {
const isBR = child.nodeName === "BR";
if (!isBR && child.parentNode == root && isInline(child)) {
wrapper || (wrapper = createElement("DIV"));
wrapper.append(child);
} else if (isBR || wrapper) {
wrapper || (wrapper = createElement("DIV"));
fixCursor(wrapper);
if (isBR) {
child.replaceWith(wrapper);
} else {
container.insertBefore(wrapper, child);
}
wrapper = null;
}
isContainer(child) && fixContainer(child, root);
});
wrapper && container.append(fixCursor(wrapper));
return container;
};
var split = (node, offset, stopNode, root) => {
if (node instanceof Text && node !== stopNode) {
if (typeof offset !== "number") {
throw new Error("Offset must be a number to split text node!");
}
if (!node.parentNode) {
throw new Error("Cannot split text node with no parent!");
}
return split(node.parentNode, node.splitText(offset), stopNode, root);
}
let nodeAfterSplit = typeof offset === "number" ? offset < node.childNodes.length ? node.childNodes[offset] : null : offset;
const parent = node.parentNode;
if (!parent || node === stopNode || !(node instanceof Element)) {
return nodeAfterSplit;
}
const clone = node.cloneNode(false);
while (nodeAfterSplit) {
const next = nodeAfterSplit.nextSibling;
clone.append(nodeAfterSplit);
nodeAfterSplit = next;
}
if (node instanceof HTMLOListElement && getClosest(node, root, "BLOCKQUOTE")) {
clone.start = (+node.start || 1) + node.childNodes.length - 1;
}
fixCursor(node);
fixCursor(clone);
node.after(clone);
return split(parent, clone, stopNode, root);
};
var _mergeInlines = (node, fakeRange) => {
const children = node.childNodes;
let l = children.length;
const frags = [];
while (l--) {
const child = children[l];
const prev = l ? children[l - 1] : null;
if (prev && isInline(child) && areAlike(child, prev)) {
if (fakeRange.startContainer === child) {
fakeRange.startContainer = prev;
fakeRange.startOffset += getLength(prev);
}
if (fakeRange.endContainer === child) {
fakeRange.endContainer = prev;
fakeRange.endOffset += getLength(prev);
}
if (fakeRange.startContainer === node) {
if (fakeRange.startOffset > l) {
--fakeRange.startOffset;
} else if (fakeRange.startOffset === l) {
fakeRange.startContainer = prev;
fakeRange.startOffset = getLength(prev);
}
}
if (fakeRange.endContainer === node) {
if (fakeRange.endOffset > l) {
--fakeRange.endOffset;
} else if (fakeRange.endOffset === l) {
fakeRange.endContainer = prev;
fakeRange.endOffset = getLength(prev);
}
}
detach(child);
if (child instanceof Text) {
prev.appendData(child.data);
} else {
frags.push(empty(child));
}
} else if (child instanceof Element) {
let frag;
while (frag = frags.pop()) {
child.append(frag);
}
_mergeInlines(child, fakeRange);
}
}
};
var mergeInlines = (node, range) => {
const element = node instanceof Text ? node.parentNode : node;
if (element instanceof Element) {
const fakeRange = {
startContainer: range.startContainer,
startOffset: range.startOffset,
endContainer: range.endContainer,
endOffset: range.endOffset
};
_mergeInlines(element, fakeRange);
range.setStart(fakeRange.startContainer, fakeRange.startOffset);
range.setEnd(fakeRange.endContainer, fakeRange.endOffset);
}
};
var mergeWithBlock = (block, next, range, root) => {
let container = next;
let parent;
let offset;
while ((parent = container.parentNode) && parent !== root && parent instanceof Element && parent.childNodes.length === 1) {
container = parent;
}
detach(container);
offset = block.childNodes.length;
const last = block.lastChild;
if (last && last.nodeName === "BR") {
last.remove();
--offset;
}
block.append(empty(next));
range.setStart(block, offset);
range.collapse(true);
mergeInlines(block, range);
};
var mergeContainers = (node, root) => {
const prev = node.previousSibling;
const first = node.firstChild;
const isListItem = node.nodeName === "LI";
if (isListItem && (!first || !/^[OU]L$/.test(first.nodeName))) {
return;
}
if (prev && areAlike(prev, node)) {
if (!isContainer(prev)) {
if (!isListItem) {
return;
}
const block = createElement("DIV");
block.append(empty(prev));
prev.append(block);
}
detach(node);
const needsFix = !isContainer(node);
prev.append(empty(node));
needsFix && fixContainer(prev, root);
first && mergeContainers(first, root);
} else if (isListItem) {
const block = createElement("DIV");
node.insertBefore(block, first);
fixCursor(block);
}
};
// source/Clean.ts
var styleToSemantic = {
"font-weight": {
@ -731,23 +534,20 @@
}
}
};
var cleanupBRs = (node, root, keepForBlankLine) => {
var cleanupBRs = (node) => {
const brs = node.querySelectorAll("BR");
const brBreaksLine = [];
let l = brs.length;
for (let i = 0; i < l; ++i) {
brBreaksLine[i] = isLineBreak(brs[i], keepForBlankLine);
brBreaksLine[i] = isLineBreak(brs[i], false);
}
while (l--) {
const br = brs[l];
const parent = br.parentNode;
if (!parent) {
continue;
}
if (!brBreaksLine[l]) {
detach(br);
} else if (!isInline(parent)) {
fixContainer(parent, root);
if (parent) {
if (!brBreaksLine[l]) {
detach(br);
}
}
}
};
@ -755,6 +555,164 @@
return text.split("&").join("&amp;").split("<").join("&lt;").split(">").join("&gt;").split('"').join("&quot;");
};
// source/node/MergeSplit.ts
var fixCursor = (node) => {
if ((node instanceof Element || node instanceof DocumentFragment) && !isInline(node) && !node.children.length && !node.textContent.length) {
node.appendChild(createElement("BR"));
}
return node;
};
var fixContainer = (container) => {
let wrapper = null;
[...container.childNodes].forEach((child) => {
if (isInline(child)) {
wrapper || (wrapper = createElement("DIV"));
wrapper.append(child);
} else if (wrapper) {
(wrapper.children.length || wrapper.textContent.trim().length) && container.insertBefore(wrapper, child);
wrapper = null;
}
});
wrapper && container.append(wrapper);
return container;
};
var split = (node, offset, stopNode, root) => {
if (node instanceof Text && node !== stopNode) {
if (typeof offset !== "number") {
throw new Error("Offset must be a number to split text node!");
}
if (!node.parentNode) {
throw new Error("Cannot split text node with no parent!");
}
return split(node.parentNode, node.splitText(offset), stopNode, root);
}
let nodeAfterSplit = typeof offset === "number" ? offset < node.childNodes.length ? node.childNodes[offset] : null : offset;
const parent = node.parentNode;
if (!parent || node === stopNode || !(node instanceof Element)) {
return nodeAfterSplit;
}
const clone = node.cloneNode(false);
while (nodeAfterSplit) {
const next = nodeAfterSplit.nextSibling;
clone.append(nodeAfterSplit);
nodeAfterSplit = next;
}
if (node instanceof HTMLOListElement && getClosest(node, root, "BLOCKQUOTE")) {
clone.start = (+node.start || 1) + node.childNodes.length - 1;
}
fixCursor(node);
fixCursor(clone);
node.after(clone);
return split(parent, clone, stopNode, root);
};
var _mergeInlines = (node, fakeRange) => {
const children = node.childNodes;
let l = children.length;
const frags = [];
while (l--) {
const child = children[l];
const prev = l ? children[l - 1] : null;
if (prev && isInline(child) && areAlike(child, prev)) {
if (fakeRange.startContainer === child) {
fakeRange.startContainer = prev;
fakeRange.startOffset += getLength(prev);
}
if (fakeRange.endContainer === child) {
fakeRange.endContainer = prev;
fakeRange.endOffset += getLength(prev);
}
if (fakeRange.startContainer === node) {
if (fakeRange.startOffset > l) {
--fakeRange.startOffset;
} else if (fakeRange.startOffset === l) {
fakeRange.startContainer = prev;
fakeRange.startOffset = getLength(prev);
}
}
if (fakeRange.endContainer === node) {
if (fakeRange.endOffset > l) {
--fakeRange.endOffset;
} else if (fakeRange.endOffset === l) {
fakeRange.endContainer = prev;
fakeRange.endOffset = getLength(prev);
}
}
detach(child);
if (child instanceof Text) {
prev.appendData(child.data);
} else {
frags.push(empty(child));
}
} else if (child instanceof Element) {
let frag;
while (frag = frags.pop()) {
child.append(frag);
}
_mergeInlines(child, fakeRange);
}
}
};
var mergeInlines = (node, range) => {
const element = node instanceof Text ? node.parentNode : node;
if (element instanceof Element) {
const fakeRange = {
startContainer: range.startContainer,
startOffset: range.startOffset,
endContainer: range.endContainer,
endOffset: range.endOffset
};
_mergeInlines(element, fakeRange);
range.setStart(fakeRange.startContainer, fakeRange.startOffset);
range.setEnd(fakeRange.endContainer, fakeRange.endOffset);
}
};
var mergeWithBlock = (block, next, range, root) => {
let container = next;
let parent;
let offset;
while ((parent = container.parentNode) && parent !== root && parent instanceof Element && parent.childNodes.length === 1) {
container = parent;
}
detach(container);
offset = block.childNodes.length;
const last = block.lastChild;
if (last && last.nodeName === "BR") {
last.remove();
--offset;
}
block.append(empty(next));
range.setStart(block, offset);
range.collapse(true);
mergeInlines(block, range);
};
var mergeContainers = (node, root) => {
const prev = node.previousSibling;
const first = node.firstChild;
const isListItem = node.nodeName === "LI";
if (isListItem && (!first || !/^[OU]L$/.test(first.nodeName))) {
return;
}
if (prev && areAlike(prev, node)) {
if (!isContainer(prev)) {
if (!isListItem) {
return;
}
const block = createElement("DIV");
block.append(empty(prev));
prev.append(block);
}
detach(node);
const needsFix = !isContainer(node);
prev.append(empty(node));
needsFix && fixContainer(prev);
first && mergeContainers(first, root);
} else if (isListItem) {
const block = createElement("DIV");
node.insertBefore(block, first);
fixCursor(block);
}
};
// source/node/Block.ts
var getBlockWalker = (node, root) => {
const walker = createTreeWalker(root, SHOW_ELEMENT, isBlock);
@ -1059,7 +1017,7 @@
var insertTreeFragmentIntoRange = (range, frag, root) => {
const firstInFragIsInline = frag.firstChild && isInline(frag.firstChild);
let node;
fixContainer(frag, root);
fixContainer(frag);
node = frag;
while (node = getNextBlock(node, root)) {
fixCursor(node);
@ -1080,7 +1038,7 @@
range.collapse(true);
let container = range.endContainer;
let offset = range.endOffset;
cleanupBRs(block, root, false);
cleanupBRs(block);
if (isInline(container)) {
const nodeAfterSplit = split(
container,
@ -1480,7 +1438,7 @@
return;
}
let current = startBlock;
fixContainer(current.parentNode, root);
fixContainer(current.parentNode);
const previous = getPreviousBlock(current, root);
if (previous) {
if (!previous.isContentEditable) {
@ -1547,7 +1505,7 @@
if (!current) {
return;
}
fixContainer(current.parentNode, root);
fixContainer(current.parentNode);
next = getNextBlock(current, root);
if (next) {
if (!next.isContentEditable) {
@ -1945,8 +1903,6 @@
_makeConfig(userConfig) {
const config = {
blockTag: "DIV",
blockAttributes: null,
tagAttributes: {},
classNames: {
color: "color",
fontFamily: "font",
@ -2549,8 +2505,8 @@
const frag = this._config.sanitizeToDOMFragment(html, this);
const root = this._root;
cleanTree(frag, this._config);
cleanupBRs(frag, root, false);
fixContainer(frag, root);
cleanupBRs(frag);
fixContainer(frag);
let node = frag;
let child = node.firstChild;
if (!child || child.nodeName === "BR") {
@ -2596,7 +2552,7 @@
this.addDetectedLinks(frag, frag);
}
cleanTree(frag, this._config);
cleanupBRs(frag, root, false);
cleanupBRs(frag);
removeEmptyInlines(frag);
frag.normalize();
let node = frag;
@ -2728,13 +2684,8 @@
const lines = plainText.split("\n");
const config = this._config;
const tag = config.blockTag;
const attributes = config.blockAttributes;
const closeBlock = "</" + tag + ">";
let openBlock = "<" + tag;
for (const attr in attributes) {
openBlock += " " + attr + '="' + escapeHTML(attributes[attr]) + '"';
}
openBlock += ">";
const openBlock = "<" + tag + ">";
for (let i = 0, l = lines.length; i < l; ++i) {
let line = lines[i];
line = escapeHTML(line).replace(/ (?=(?: |$))/g, "&nbsp;");
@ -3034,7 +2985,6 @@
{
href: url
},
this._config.tagAttributes.a,
attributes
);
return this.changeFormat(
@ -3065,7 +3015,6 @@
(node2) => !getClosest(node2, root || this._root, "A")
);
const linkRegExp = this.linkRegExp;
const defaultAttributes = this._config.tagAttributes.a;
let node;
while (node = walker.nextNode()) {
const parent = node.parentNode;
@ -3082,12 +3031,9 @@
}
const child = createElement(
"A",
Object.assign(
{
href: match[1] ? /^(?:ht|f)tps?:/i.test(match[1]) ? match[1] : "http://" + match[1] : "mailto:" + match[0]
},
defaultAttributes
)
{
href: match[1] ? /^(?:ht|f)tps?:/i.test(match[1]) ? match[1] : "http://" + match[1] : "mailto:" + match[0]
}
);
child.textContent = data.slice(index, endIndex);
parent.insertBefore(child, node);
@ -3107,7 +3053,7 @@
createDefaultBlock(children) {
const config = this._config;
return fixCursor(
createElement(config.blockTag, config.blockAttributes, children)
createElement(config.blockTag, null, children)
);
}
splitBlock(lineBreakOnly, range) {
@ -3197,21 +3143,15 @@
}
node = range.startContainer;
const offset = range.startOffset;
let splitTag = this.tagAfterSplit[block.nodeName];
let splitTag = this.tagAfterSplit[block.nodeName] || this._config.blockTag;
nodeAfterSplit = split(
node,
offset,
block.parentNode,
this._root
);
const config = this._config;
let splitProperties = null;
if (!splitTag) {
splitTag = config.blockTag;
splitProperties = config.blockAttributes;
}
if (!hasTagAttributes(nodeAfterSplit, splitTag, splitProperties)) {
block = createElement(splitTag, splitProperties);
if (!hasTagAttributes(nodeAfterSplit, splitTag)) {
block = createElement(splitTag);
if (nodeAfterSplit.dir) {
block.dir = nodeAfterSplit.dir;
}
@ -3376,11 +3316,9 @@
this._recordUndoState(range, this._isInUndoState);
const type = list.nodeName;
let newParent = startLi.previousSibling;
let listAttrs;
let next;
if (newParent.nodeName !== type) {
listAttrs = this._config.tagAttributes[type.toLowerCase()];
newParent = createElement(type, listAttrs);
newParent = createElement(type);
list.insertBefore(newParent, startLi);
}
do {
@ -3450,9 +3388,6 @@
}
_makeList(frag, type) {
const walker = getBlockWalker(frag, this._root);
const tagAttributes = this._config.tagAttributes;
const listAttrs = tagAttributes[type.toLowerCase()];
const listItemAttrs = tagAttributes.li;
let node;
while (node = walker.nextNode()) {
if (node.parentNode instanceof HTMLLIElement) {
@ -3460,7 +3395,7 @@
walker.currentNode = node.lastChild;
}
if (!(node instanceof HTMLLIElement)) {
const newLi = createElement("LI", listItemAttrs);
const newLi = createElement("LI");
if (node.dir) {
newLi.dir = node.dir;
}
@ -3469,7 +3404,7 @@
prev.append(newLi);
detach(node);
} else {
replaceWith(node, createElement(type, listAttrs, [newLi]));
replaceWith(node, createElement(type, null, [newLi]));
}
newLi.append(empty(node));
walker.currentNode = newLi;
@ -3479,7 +3414,7 @@
if (tag !== type && /^[OU]L$/.test(tag)) {
replaceWith(
node,
createElement(type, listAttrs, [empty(node)])
createElement(type, null, [empty(node)])
);
}
}
@ -3502,7 +3437,7 @@
for (let i = 0, l = lists.length; i < l; ++i) {
const list = lists[i];
const listFrag = empty(list);
fixContainer(listFrag, root);
fixContainer(listFrag);
replaceWith(list, listFrag);
}
for (let i = 0, l = items.length; i < l; ++i) {
@ -3510,7 +3445,7 @@
if (isBlock(item)) {
replaceWith(item, this.createDefaultBlock([empty(item)]));
} else {
fixContainer(item, root);
fixContainer(item);
replaceWith(item, empty(item));
}
}
@ -3523,7 +3458,7 @@
this.modifyBlocks(
(frag) => createElement(
"BLOCKQUOTE",
this._config.tagAttributes.blockquote,
null,
[frag]
),
range
@ -3608,7 +3543,7 @@
}
output.normalize();
return fixCursor(
createElement("PRE", this._config.tagAttributes.pre, [
createElement("PRE", null, [
output
])
);
@ -3618,7 +3553,7 @@
this.changeFormat(
{
tag: "CODE",
attributes: this._config.tagAttributes.code
attributes: null
},
null,
range
@ -3654,7 +3589,7 @@
node.parentNode.insertBefore(contents, node);
node.data = value;
}
fixContainer(pre, root);
fixContainer(pre);
replaceWith(pre, empty(pre));
}
return frag;