vditor
Version:
♏ 易于使用的 Markdown 编辑器,为适配不同的应用场景而生
1,175 lines (1,096 loc) • 66.6 kB
text/typescript
import {Constants} from "../constants";
import {input as IRInput} from "../ir/input";
import {processAfterRender} from "../ir/process";
import {processAfterRender as processSVAfterRender, processPaste} from "../sv/process";
import {uploadFiles} from "../upload/index";
import {setHeaders} from "../upload/setHeaders";
import {afterRenderEvent} from "../wysiwyg/afterRenderEvent";
import {input} from "../wysiwyg/input";
import {isCtrl, isFirefox} from "./compatibility";
import {scrollCenter} from "./editorCommonEvent";
import {
getTopList,
hasClosestBlock,
hasClosestByAttribute,
hasClosestByClassName,
hasClosestByMatchTag,
} from "./hasClosest";
import {getLastNode} from "./hasClosest";
import {highlightToolbar} from "./highlightToolbar";
import {matchHotKey} from "./hotKey";
import {processCodeRender, processPasteCode} from "./processCode";
import {
getEditorRange,
getSelectPosition,
insertHTML,
setRangeByWbr,
setSelectionByPosition, setSelectionFocus,
} from "./selection";
// https://github.com/Vanessa219/vditor/issues/508 软键盘无法删除空块
export const fixGSKeyBackspace = (event: KeyboardEvent, vditor: IVditor, startContainer: Node) => {
if (event.keyCode === 229 && event.code === "" && event.key === "Unidentified" && vditor.currentMode !== "sv") {
const blockElement = hasClosestBlock(startContainer);
// 移动端的标点符号都显示为 299,因此需限定为空删除的条件
if (blockElement && blockElement.textContent.trim() === "") {
vditor[vditor.currentMode].composingLock = true;
return false;
}
}
return true;
};
// https://github.com/Vanessa219/vditor/issues/361 代码块后输入中文
export const fixCJKPosition = (range: Range, vditor: IVditor, event: KeyboardEvent) => {
if (event.key === "Enter" || event.key === "Tab" || event.key === "Backspace" || event.key.indexOf("Arrow") > -1
|| isCtrl(event) || event.key === "Escape" || event.shiftKey || event.altKey) {
return;
}
const pLiElement = hasClosestByMatchTag(range.startContainer, "P") ||
hasClosestByMatchTag(range.startContainer, "LI");
if (pLiElement && getSelectPosition(pLiElement, vditor[vditor.currentMode].element, range).start === 0) {
// https://github.com/Vanessa219/vditor/issues/1289 WKWebView切换输入法产生六分之一空格,造成光标错位
if (pLiElement.nodeValue) {
pLiElement.nodeValue = pLiElement.nodeValue.replace(/\u2006/g, '');
}
const zwspNode = document.createTextNode(Constants.ZWSP);
range.insertNode(zwspNode);
range.setStartAfter(zwspNode);
}
};
// https://github.com/Vanessa219/vditor/issues/381 光标在内联数学公式中无法向下移动
export const fixCursorDownInlineMath = (range: Range, key: string) => {
if (key === "ArrowDown" || key === "ArrowUp") {
const inlineElement = hasClosestByAttribute(range.startContainer, "data-type", "math-inline") ||
hasClosestByAttribute(range.startContainer, "data-type", "html-entity") ||
hasClosestByAttribute(range.startContainer, "data-type", "html-inline");
if (inlineElement) {
if (key === "ArrowDown") {
range.setStartAfter(inlineElement.parentElement);
}
if (key === "ArrowUp") {
range.setStartBefore(inlineElement.parentElement);
}
}
}
};
export const insertEmptyBlock = (vditor: IVditor, position: InsertPosition) => {
const range = getEditorRange(vditor);
const blockElement = hasClosestBlock(range.startContainer);
if (blockElement) {
blockElement.insertAdjacentHTML(position, `<p data-block="0">${Constants.ZWSP}<wbr>\n</p>`);
setRangeByWbr(vditor[vditor.currentMode].element, range);
highlightToolbar(vditor);
execAfterRender(vditor);
}
};
export const isFirstCell = (cellElement: HTMLElement) => {
const tableElement = hasClosestByMatchTag(cellElement, "TABLE") as HTMLTableElement;
if (tableElement && tableElement.rows[0].cells[0].isSameNode(cellElement)) {
return tableElement;
}
return false;
};
export const isLastCell = (cellElement: HTMLElement) => {
const tableElement = hasClosestByMatchTag(cellElement, "TABLE") as HTMLTableElement;
if (tableElement && tableElement.lastElementChild.lastElementChild.lastElementChild.isSameNode(cellElement)) {
return tableElement;
}
return false;
};
// 光标设置到前一个表格中
const goPreviousCell = (cellElement: HTMLElement, range: Range, isSelected = true) => {
let previousElement = cellElement.previousElementSibling;
if (!previousElement) {
if (cellElement.parentElement.previousElementSibling) {
previousElement = cellElement.parentElement.previousElementSibling.lastElementChild;
} else if (cellElement.parentElement.parentElement.tagName === "TBODY" &&
cellElement.parentElement.parentElement.previousElementSibling) {
previousElement = cellElement.parentElement
.parentElement.previousElementSibling.lastElementChild.lastElementChild;
} else {
previousElement = null;
}
}
if (previousElement) {
range.selectNodeContents(previousElement);
if (!isSelected) {
range.collapse(false);
}
setSelectionFocus(range);
}
return previousElement;
};
export const insertAfterBlock = (vditor: IVditor, event: KeyboardEvent, range: Range, element: HTMLElement,
blockElement: HTMLElement) => {
const position = getSelectPosition(element, vditor[vditor.currentMode].element, range);
if ((event.key === "ArrowDown" && element.textContent.trimRight().substr(position.start).indexOf("\n") === -1) ||
(event.key === "ArrowRight" && position.start >= element.textContent.trimRight().length)) {
const nextElement = blockElement.nextElementSibling;
if (!nextElement ||
(nextElement && (nextElement.tagName === "TABLE" || nextElement.getAttribute("data-type")))) {
blockElement.insertAdjacentHTML("afterend",
`<p data-block="0">${Constants.ZWSP}<wbr></p>`);
setRangeByWbr(vditor[vditor.currentMode].element, range);
} else {
range.selectNodeContents(nextElement);
range.collapse(true);
setSelectionFocus(range);
}
event.preventDefault();
return true;
}
return false;
};
export const insertBeforeBlock = (vditor: IVditor, event: KeyboardEvent, range: Range, element: HTMLElement,
blockElement: HTMLElement) => {
const position = getSelectPosition(element, vditor[vditor.currentMode].element, range);
if ((event.key === "ArrowUp" && element.textContent.substr(0, position.start).indexOf("\n") === -1) ||
((event.key === "ArrowLeft" || (event.key === "Backspace" && range.toString() === "")) &&
position.start === 0)) {
const previousElement = blockElement.previousElementSibling;
// table || code
if (!previousElement ||
(previousElement && (previousElement.tagName === "TABLE" || previousElement.getAttribute("data-type")))) {
blockElement.insertAdjacentHTML("beforebegin",
`<p data-block="0">${Constants.ZWSP}<wbr></p>`);
setRangeByWbr(vditor[vditor.currentMode].element, range);
} else {
range.selectNodeContents(previousElement);
range.collapse(false);
setSelectionFocus(range);
}
event.preventDefault();
return true;
}
return false;
};
export const listToggle = (vditor: IVditor, range: Range, type: string, cancel = true) => {
const itemElement = hasClosestByMatchTag(range.startContainer, "LI");
vditor[vditor.currentMode].element.querySelectorAll("wbr").forEach((wbr) => {
wbr.remove();
});
range.insertNode(document.createElement("wbr"));
if (cancel && itemElement) {
// 取消
let pHTML = "";
for (let i = 0; i < itemElement.parentElement.childElementCount; i++) {
const inputElement = itemElement.parentElement.children[i].querySelector("input");
if (inputElement) {
inputElement.remove();
}
pHTML += `<p data-block="0">${itemElement.parentElement.children[i].innerHTML.trimLeft()}</p>`;
}
itemElement.parentElement.insertAdjacentHTML("beforebegin", pHTML);
itemElement.parentElement.remove();
} else {
if (!itemElement) {
// 添加
let blockElement = hasClosestByAttribute(range.startContainer, "data-block", "0");
if (!blockElement) {
vditor[vditor.currentMode].element.querySelector("wbr").remove();
blockElement = vditor[vditor.currentMode].element.querySelector("p");
blockElement.innerHTML = "<wbr>";
}
if (type === "check") {
blockElement.insertAdjacentHTML("beforebegin",
`<ul data-block="0"><li class="vditor-task"><input type="checkbox" /> ${blockElement.innerHTML}</li></ul>`);
blockElement.remove();
} else if (type === "list") {
blockElement.insertAdjacentHTML("beforebegin",
`<ul data-block="0"><li>${blockElement.innerHTML}</li></ul>`);
blockElement.remove();
} else if (type === "ordered-list") {
blockElement.insertAdjacentHTML("beforebegin",
`<ol data-block="0"><li>${blockElement.innerHTML}</li></ol>`);
blockElement.remove();
}
} else {
// 切换
if (type === "check") {
itemElement.parentElement.querySelectorAll("li").forEach((item) => {
item.insertAdjacentHTML("afterbegin",
`<input type="checkbox" />${item.textContent.indexOf(" ") === 0 ? "" : " "}`);
item.classList.add("vditor-task");
});
} else {
if (itemElement.querySelector("input")) {
itemElement.parentElement.querySelectorAll("li").forEach((item) => {
item.querySelector("input").remove();
item.classList.remove("vditor-task");
});
}
let element;
if (type === "list") {
element = document.createElement("ul");
element.setAttribute("data-marker", "*");
} else {
element = document.createElement("ol");
element.setAttribute("data-marker", "1.");
}
element.setAttribute("data-block", "0");
element.setAttribute("data-tight", itemElement.parentElement.getAttribute("data-tight"));
element.innerHTML = itemElement.parentElement.innerHTML;
itemElement.parentElement.parentNode.replaceChild(element, itemElement.parentElement);
}
}
}
};
export const listIndent = (vditor: IVditor, liElement: HTMLElement, range: Range) => {
const previousElement = liElement.previousElementSibling;
if (liElement && previousElement) {
const liElements: HTMLElement[] = [liElement];
Array.from(range.cloneContents().children).forEach((item, index) => {
if (item.nodeType !== 3 && liElement && item.textContent.trim() !== ""
&& liElement.getAttribute("data-node-id") === item.getAttribute("data-node-id")) {
if (index !== 0) {
liElements.push(liElement);
}
liElement = liElement.nextElementSibling as HTMLElement;
}
});
vditor[vditor.currentMode].element.querySelectorAll("wbr").forEach((wbr) => {
wbr.remove();
});
range.insertNode(document.createElement("wbr"));
const liParentElement = previousElement.parentElement;
let liHTML = "";
liElements.forEach((item: HTMLElement) => {
let marker = item.getAttribute("data-marker");
if (marker.length !== 1) {
marker = `1${marker.slice(-1)}`;
}
liHTML += `<li data-node-id="${item.getAttribute("data-node-id")}" data-marker="${marker}">${item.innerHTML}</li>`;
item.remove();
});
previousElement.insertAdjacentHTML("beforeend",
`<${liParentElement.tagName} data-block="0">${liHTML}</${liParentElement.tagName}>`);
if (vditor.currentMode === "wysiwyg") {
liParentElement.outerHTML = vditor.lute.SpinVditorDOM(liParentElement.outerHTML);
} else {
liParentElement.outerHTML = vditor.lute.SpinVditorIRDOM(liParentElement.outerHTML);
}
setRangeByWbr(vditor[vditor.currentMode].element, range);
const tempTopListElement = getTopList(range.startContainer);
if (tempTopListElement) {
tempTopListElement.querySelectorAll(`.vditor-${vditor.currentMode}__preview[data-render='2']`)
.forEach((item: HTMLElement) => {
processCodeRender(item, vditor);
if (vditor.currentMode === "wysiwyg") {
item.previousElementSibling.setAttribute("style", "display:none");
}
});
}
execAfterRender(vditor);
highlightToolbar(vditor);
} else {
vditor[vditor.currentMode].element.focus();
}
};
export const listOutdent = (vditor: IVditor, liElement: HTMLElement, range: Range, topListElement: HTMLElement) => {
const liParentLiElement = hasClosestByMatchTag(liElement.parentElement, "LI");
if (liParentLiElement) {
vditor[vditor.currentMode].element.querySelectorAll("wbr").forEach((wbr) => {
wbr.remove();
});
range.insertNode(document.createElement("wbr"));
const liParentElement = liElement.parentElement;
const liParentAfterElement = liParentElement.cloneNode() as HTMLElement;
const liElements: HTMLElement[] = [liElement];
Array.from(range.cloneContents().children).forEach((item, index) => {
if (item.nodeType !== 3 && liElement && item.textContent.trim() !== "" &&
liElement.getAttribute("data-node-id") === item.getAttribute("data-node-id")) {
if (index !== 0) {
liElements.push(liElement);
}
liElement = liElement.nextElementSibling as HTMLElement;
}
});
let isMatch = false;
let afterHTML = "";
liParentElement.querySelectorAll("li").forEach((item) => {
if (isMatch) {
afterHTML += item.outerHTML;
if (!item.nextElementSibling && !item.previousElementSibling) {
item.parentElement.remove();
} else {
item.remove();
}
}
if (item.isSameNode(liElements[liElements.length - 1])) {
isMatch = true;
}
});
liElements.reverse().forEach((item) => {
liParentLiElement.insertAdjacentElement("afterend", item);
});
if (afterHTML) {
liParentAfterElement.innerHTML = afterHTML;
liElements[0].insertAdjacentElement("beforeend", liParentAfterElement);
}
if (vditor.currentMode === "wysiwyg") {
topListElement.outerHTML = vditor.lute.SpinVditorDOM(topListElement.outerHTML);
} else {
topListElement.outerHTML = vditor.lute.SpinVditorIRDOM(topListElement.outerHTML);
}
setRangeByWbr(vditor[vditor.currentMode].element, range);
const tempTopListElement = getTopList(range.startContainer);
if (tempTopListElement) {
tempTopListElement.querySelectorAll(`.vditor-${vditor.currentMode}__preview[data-render='2']`)
.forEach((item: HTMLElement) => {
processCodeRender(item, vditor);
if (vditor.currentMode === "wysiwyg") {
item.previousElementSibling.setAttribute("style", "display:none");
}
});
}
execAfterRender(vditor);
highlightToolbar(vditor);
} else {
vditor[vditor.currentMode].element.focus();
}
};
export const setTableAlign = (tableElement: HTMLTableElement, type: string) => {
const cell = getSelection().getRangeAt(0).startContainer.parentElement;
const columnCnt = tableElement.rows[0].cells.length;
const rowCnt = tableElement.rows.length;
let currentColumn = 0;
for (let i = 0; i < rowCnt; i++) {
for (let j = 0; j < columnCnt; j++) {
if (tableElement.rows[i].cells[j].isSameNode(cell)) {
currentColumn = j;
break;
}
}
}
for (let k = 0; k < rowCnt; k++) {
tableElement.rows[k].cells[currentColumn].setAttribute("align", type);
}
};
export const isHrMD = (text: string) => {
// - _ *
const marker = text.trimRight().split("\n").pop();
if (marker === "") {
return false;
}
if (marker.replace(/ |-/g, "") === ""
|| marker.replace(/ |_/g, "") === ""
|| marker.replace(/ |\*/g, "") === "") {
if (marker.replace(/ /g, "").length > 2) {
if (marker.indexOf("-") > -1 && marker.trimLeft().indexOf(" ") === -1
&& text.trimRight().split("\n").length > 1) {
// 满足 heading
return false;
}
if (marker.indexOf(" ") === 0 || marker.indexOf("\t") === 0) {
// 代码块
return false;
}
return true;
}
return false;
}
return false;
};
export const isHeadingMD = (text: string) => {
// - =
const textArray = text.trimRight().split("\n");
text = textArray.pop();
if (text.indexOf(" ") === 0 || text.indexOf("\t") === 0) {
return false;
}
text = text.trimLeft();
if (text === "" || textArray.length === 0) {
return false;
}
if (text.replace(/-/g, "") === ""
|| text.replace(/=/g, "") === "") {
return true;
}
return false;
};
export const execAfterRender = (vditor: IVditor, options = {
enableAddUndoStack: true,
enableHint: false,
enableInput: true,
}) => {
if (vditor.currentMode === "wysiwyg") {
afterRenderEvent(vditor, options);
} else if (vditor.currentMode === "ir") {
processAfterRender(vditor, options);
} else if (vditor.currentMode === "sv") {
processSVAfterRender(vditor, options);
}
};
export const fixList = (range: Range, vditor: IVditor, pElement: HTMLElement | false, event: KeyboardEvent) => {
const startContainer = range.startContainer;
const liElement = hasClosestByMatchTag(startContainer, "LI");
if (liElement) {
if (!isCtrl(event) && !event.altKey && event.key === "Enter" &&
// fix li 中有多个 P 时,在第一个 P 中换行会在下方生成新的 li
(!event.shiftKey && pElement && liElement.contains(pElement) && pElement.nextElementSibling)) {
if (liElement && !liElement.textContent.endsWith("\n")) {
// li 结尾需 \n
liElement.insertAdjacentText("beforeend", "\n");
}
range.insertNode(document.createTextNode("\n\n"));
range.collapse(false);
execAfterRender(vditor);
event.preventDefault();
return true;
}
if (!isCtrl(event) && !event.shiftKey && !event.altKey && event.key === "Backspace" &&
!liElement.previousElementSibling && range.toString() === "" &&
getSelectPosition(liElement, vditor[vditor.currentMode].element, range).start === 0) {
// 光标位于点和第一个字符中间时,无法删除 li 元素
if (liElement.nextElementSibling) {
liElement.parentElement.insertAdjacentHTML("beforebegin",
`<p data-block="0"><wbr>${liElement.innerHTML}</p>`);
liElement.remove();
} else {
liElement.parentElement.outerHTML = `<p data-block="0"><wbr>${liElement.innerHTML}</p>`;
}
setRangeByWbr(vditor[vditor.currentMode].element, range);
execAfterRender(vditor);
event.preventDefault();
return true;
}
// 空列表删除后与上一级段落对齐
if (!isCtrl(event) && !event.shiftKey && !event.altKey && event.key === "Backspace" &&
liElement.textContent.trim().replace(Constants.ZWSP, "") === "" &&
range.toString() === "" && liElement.previousElementSibling?.tagName === "LI") {
liElement.previousElementSibling.insertAdjacentText("beforeend", "\n\n");
range.selectNodeContents(liElement.previousElementSibling);
range.collapse(false);
liElement.remove();
setRangeByWbr(vditor[vditor.currentMode].element, range);
execAfterRender(vditor);
event.preventDefault();
return true;
}
if (!isCtrl(event) && !event.altKey && event.key === "Tab") {
// 光标位于第一/零字符时,tab 用于列表的缩进
let isFirst = false;
if (range.startOffset === 0
&& ((startContainer.nodeType === 3 && !startContainer.previousSibling)
|| (startContainer.nodeType !== 3 && startContainer.nodeName === "LI"))) {
// 有序/无序列表
isFirst = true;
} else if (liElement.classList.contains("vditor-task") && range.startOffset === 1
&& startContainer.previousSibling.nodeType !== 3
&& (startContainer.previousSibling as HTMLElement).tagName === "INPUT") {
// 任务列表
isFirst = true;
}
if (isFirst || range.toString() !== "") {
if (event.shiftKey) {
listOutdent(vditor, liElement, range, liElement.parentElement);
} else {
listIndent(vditor, liElement, range);
}
event.preventDefault();
return true;
}
}
}
return false;
};
// tab 处理: block code render, table, 列表第一个字符中的 tab 处理单独写在上面
export const fixTab = (vditor: IVditor, range: Range, event: KeyboardEvent) => {
if (vditor.options.tab && event.key === "Tab") {
if (event.shiftKey) {
// TODO shift+tab
} else {
if (range.toString() === "") {
range.insertNode(document.createTextNode(vditor.options.tab));
range.collapse(false);
} else {
range.extractContents();
range.insertNode(document.createTextNode(vditor.options.tab));
range.collapse(false);
}
}
setSelectionFocus(range);
execAfterRender(vditor);
event.preventDefault();
return true;
}
};
export const fixMarkdown = (event: KeyboardEvent, vditor: IVditor, pElement: HTMLElement | false, range: Range) => {
if (!pElement) {
return;
}
if (!isCtrl(event) && !event.altKey && event.key === "Enter") {
const pText = String.raw`${pElement.textContent}`.replace(/\\\|/g, "").trim();
const pTextList = pText.split("|");
if (pText.startsWith("|") && pText.endsWith("|") && pTextList.length > 3) {
// table 自动完成
let tableHeaderMD = pTextList.map(() => "---").join("|");
tableHeaderMD =
pElement.textContent + "\n" + tableHeaderMD.substring(3, tableHeaderMD.length - 3) + "\n|<wbr>";
pElement.outerHTML = vditor.lute.SpinVditorDOM(tableHeaderMD);
setRangeByWbr(vditor[vditor.currentMode].element, range);
execAfterRender(vditor);
scrollCenter(vditor);
event.preventDefault();
return true;
}
// hr 渲染
if (isHrMD(pElement.innerHTML) && pElement.previousElementSibling) {
// 软换行后 hr 前有内容
let pInnerHTML = "";
const innerHTMLList = pElement.innerHTML.trimRight().split("\n");
if (innerHTMLList.length > 1) {
innerHTMLList.pop();
pInnerHTML = `<p data-block="0">${innerHTMLList.join("\n")}</p>`;
}
pElement.insertAdjacentHTML("afterend",
`${pInnerHTML}<hr data-block="0"><p data-block="0"><wbr>\n</p>`);
pElement.remove();
setRangeByWbr(vditor[vditor.currentMode].element, range);
execAfterRender(vditor);
scrollCenter(vditor);
event.preventDefault();
return true;
}
if (isHeadingMD(pElement.innerHTML)) {
// heading 渲染
if (vditor.currentMode === "wysiwyg") {
pElement.outerHTML = vditor.lute.SpinVditorDOM(pElement.innerHTML + '<p data-block="0"><wbr>\n</p>');
} else {
pElement.outerHTML = vditor.lute.SpinVditorIRDOM(pElement.innerHTML + '<p data-block="0"><wbr>\n</p>');
}
setRangeByWbr(vditor[vditor.currentMode].element, range);
execAfterRender(vditor);
scrollCenter(vditor);
event.preventDefault();
return true;
}
}
// 软换行会被切割 https://github.com/Vanessa219/vditor/issues/220
if (range.collapsed && pElement.previousElementSibling && event.key === "Backspace" &&
!isCtrl(event) && !event.altKey && !event.shiftKey &&
pElement.textContent.trimRight().split("\n").length > 1 &&
getSelectPosition(pElement, vditor[vditor.currentMode].element, range).start === 0) {
const lastElement = getLastNode(pElement.previousElementSibling) as HTMLElement;
if (!lastElement.textContent.endsWith("\n")) {
lastElement.textContent = lastElement.textContent + "\n";
}
lastElement.parentElement.insertAdjacentHTML("beforeend", `<wbr>${pElement.innerHTML}`);
pElement.remove();
setRangeByWbr(vditor[vditor.currentMode].element, range);
return false;
}
return false;
};
export const insertRow = (vditor: IVditor, range: Range, cellElement: HTMLElement) => {
let rowHTML = "";
for (let m = 0; m < cellElement.parentElement.childElementCount; m++) {
rowHTML += `<td align="${cellElement.parentElement.children[m].getAttribute("align")}"> </td>`;
}
if (cellElement.tagName === "TH") {
cellElement.parentElement.parentElement.insertAdjacentHTML("afterend",
`<tbody><tr>${rowHTML}</tr></tbody>`);
} else {
cellElement.parentElement.insertAdjacentHTML("afterend", `<tr>${rowHTML}</tr>`);
}
execAfterRender(vditor);
};
export const insertRowAbove = (vditor: IVditor, range: Range, cellElement: HTMLElement) => {
let rowHTML = "";
for (let m = 0; m < cellElement.parentElement.childElementCount; m++) {
if (cellElement.tagName === "TH") {
rowHTML += `<th align="${cellElement.parentElement.children[m].getAttribute("align")}"> </th>`;
} else {
rowHTML += `<td align="${cellElement.parentElement.children[m].getAttribute("align")}"> </td>`;
}
}
if (cellElement.tagName === "TH") {
cellElement.parentElement.parentElement.insertAdjacentHTML("beforebegin", `<thead><tr>${rowHTML}</tr></thead>`);
range.insertNode(document.createElement("wbr"));
const theadHTML = cellElement.parentElement.innerHTML.replace(/<th>/g, "<td>").replace(/<\/th>/g, "</td>");
cellElement.parentElement.parentElement.nextElementSibling.insertAdjacentHTML("afterbegin", theadHTML);
cellElement.parentElement.parentElement.remove();
setRangeByWbr(vditor.ir.element, range);
} else {
cellElement.parentElement.insertAdjacentHTML("beforebegin", `<tr>${rowHTML}</tr>`);
}
execAfterRender(vditor);
};
export const insertColumn =
(vditor: IVditor, tableElement: HTMLTableElement, cellElement: HTMLElement, type: InsertPosition = "afterend") => {
let index = 0;
let previousElement = cellElement.previousElementSibling;
while (previousElement) {
index++;
previousElement = previousElement.previousElementSibling;
}
for (let i = 0; i < tableElement.rows.length; i++) {
if (i === 0) {
tableElement.rows[i].cells[index].insertAdjacentHTML(type, "<th> </th>");
} else {
tableElement.rows[i].cells[index].insertAdjacentHTML(type, "<td> </td>");
}
}
execAfterRender(vditor);
};
export const deleteRow = (vditor: IVditor, range: Range, cellElement: HTMLElement) => {
if (cellElement.tagName === "TD") {
const tbodyElement = cellElement.parentElement.parentElement;
if (cellElement.parentElement.previousElementSibling) {
range.selectNodeContents(cellElement.parentElement.previousElementSibling.lastElementChild);
} else {
range.selectNodeContents(tbodyElement.previousElementSibling.lastElementChild.lastElementChild);
}
if (tbodyElement.childElementCount === 1) {
tbodyElement.remove();
} else {
cellElement.parentElement.remove();
}
range.collapse(false);
setSelectionFocus(range);
execAfterRender(vditor);
}
};
export const deleteColumn =
(vditor: IVditor, range: Range, tableElement: HTMLTableElement, cellElement: HTMLElement) => {
let index = 0;
let previousElement = cellElement.previousElementSibling;
while (previousElement) {
index++;
previousElement = previousElement.previousElementSibling;
}
if (cellElement.previousElementSibling || cellElement.nextElementSibling) {
range.selectNodeContents(cellElement.previousElementSibling || cellElement.nextElementSibling);
range.collapse(true);
}
for (let i = 0; i < tableElement.rows.length; i++) {
const cells = tableElement.rows[i].cells;
if (cells.length === 1) {
tableElement.remove();
highlightToolbar(vditor);
break;
}
cells[index].remove();
}
setSelectionFocus(range);
execAfterRender(vditor);
};
export const fixTable = (vditor: IVditor, event: KeyboardEvent, range: Range) => {
const startContainer = range.startContainer;
const cellElement = hasClosestByMatchTag(startContainer, "TD") ||
hasClosestByMatchTag(startContainer, "TH");
if (cellElement) {
// 换行或软换行:在 cell 中添加 br
if (!isCtrl(event) && !event.altKey && event.key === "Enter") {
if (!cellElement.lastElementChild ||
(cellElement.lastElementChild && (!cellElement.lastElementChild.isSameNode(cellElement.lastChild) ||
cellElement.lastElementChild.tagName !== "BR"))) {
cellElement.insertAdjacentHTML("beforeend", "<br>");
}
const brElement = document.createElement("br");
range.insertNode(brElement);
range.setStartAfter(brElement);
execAfterRender(vditor);
scrollCenter(vditor);
event.preventDefault();
return true;
}
// tab:光标移向下一个 cell
if (event.key === "Tab") {
if (event.shiftKey) {
// shift + tab 光标移动到前一个 cell
goPreviousCell(cellElement, range);
event.preventDefault();
return true;
}
let nextElement = cellElement.nextElementSibling;
if (!nextElement) {
if (cellElement.parentElement.nextElementSibling) {
nextElement = cellElement.parentElement.nextElementSibling.firstElementChild;
} else if (cellElement.parentElement.parentElement.tagName === "THEAD" &&
cellElement.parentElement.parentElement.nextElementSibling) {
nextElement =
cellElement.parentElement.parentElement.nextElementSibling.firstElementChild.firstElementChild;
} else {
nextElement = null;
}
}
if (nextElement) {
range.selectNodeContents(nextElement);
setSelectionFocus(range);
}
event.preventDefault();
return true;
}
const tableElement = cellElement.parentElement.parentElement.parentElement as HTMLTableElement;
if (event.key === "ArrowUp") {
event.preventDefault();
if (cellElement.tagName === "TH") {
if (tableElement.previousElementSibling) {
range.selectNodeContents(tableElement.previousElementSibling);
range.collapse(false);
setSelectionFocus(range);
} else {
insertEmptyBlock(vditor, "beforebegin");
}
return true;
}
let m = 0;
const trElement = cellElement.parentElement as HTMLTableRowElement;
for (; m < trElement.cells.length; m++) {
if (trElement.cells[m].isSameNode(cellElement)) {
break;
}
}
let previousElement = trElement.previousElementSibling as HTMLTableRowElement;
if (!previousElement) {
previousElement = trElement.parentElement.previousElementSibling.firstChild as HTMLTableRowElement;
}
range.selectNodeContents(previousElement.cells[m]);
range.collapse(false);
setSelectionFocus(range);
return true;
}
if (event.key === "ArrowDown") {
event.preventDefault();
const trElement = cellElement.parentElement as HTMLTableRowElement;
if (!trElement.nextElementSibling && cellElement.tagName === "TD") {
if (tableElement.nextElementSibling) {
range.selectNodeContents(tableElement.nextElementSibling);
range.collapse(true);
setSelectionFocus(range);
} else {
insertEmptyBlock(vditor, "afterend");
}
return true;
}
let m = 0;
for (; m < trElement.cells.length; m++) {
if (trElement.cells[m].isSameNode(cellElement)) {
break;
}
}
let nextElement = trElement.nextElementSibling as HTMLTableRowElement;
if (!nextElement) {
nextElement = trElement.parentElement.nextElementSibling.firstChild as HTMLTableRowElement;
}
range.selectNodeContents(nextElement.cells[m]);
range.collapse(true);
setSelectionFocus(range);
return true;
}
// focus row input, only wysiwyg
if (vditor.currentMode === "wysiwyg" &&
!isCtrl(event) && event.key === "Enter" && !event.shiftKey && event.altKey) {
const inputElement = (vditor.wysiwyg.popover.querySelector(".vditor-input") as HTMLInputElement);
inputElement.focus();
inputElement.select();
event.preventDefault();
return true;
}
// Backspace:光标移动到前一个 cell
if (!isCtrl(event) && !event.shiftKey && !event.altKey && event.key === "Backspace"
&& range.startOffset === 0 && range.toString() === "") {
const previousCellElement = goPreviousCell(cellElement, range, false);
if (!previousCellElement && tableElement) {
if (tableElement.textContent.trim() === "") {
tableElement.outerHTML = `<p data-block="0"><wbr>\n</p>`;
setRangeByWbr(vditor[vditor.currentMode].element, range);
} else {
range.setStartBefore(tableElement);
range.collapse(true);
}
execAfterRender(vditor);
}
event.preventDefault();
return true;
}
// 上方新添加一行
if (matchHotKey("⇧⌘F", event)) {
insertRowAbove(vditor, range, cellElement);
event.preventDefault();
return true;
}
// 下方新添加一行 https://github.com/Vanessa219/vditor/issues/46
if (matchHotKey("⌘=", event)) {
insertRow(vditor, range, cellElement);
event.preventDefault();
return true;
}
// 左方新添加一列
if (matchHotKey("⇧⌘G", event)) {
insertColumn(vditor, tableElement, cellElement, "beforebegin");
event.preventDefault();
return true;
}
// 后方新添加一列
if (matchHotKey("⇧⌘=", event)) {
insertColumn(vditor, tableElement, cellElement);
event.preventDefault();
return true;
}
// 删除当前行
if (matchHotKey("⌘-", event)) {
deleteRow(vditor, range, cellElement);
event.preventDefault();
return true;
}
// 删除当前列
if (matchHotKey("⇧⌘-", event)) {
deleteColumn(vditor, range, tableElement, cellElement);
event.preventDefault();
return true;
}
// 剧左
if (matchHotKey("⇧⌘L", event)) {
if (vditor.currentMode === "ir") {
setTableAlign(tableElement, "left");
execAfterRender(vditor);
event.preventDefault();
return true;
} else {
const itemElement: HTMLElement = vditor.wysiwyg.popover.querySelector('[data-type="left"]');
if (itemElement) {
itemElement.click();
event.preventDefault();
return true;
}
}
}
// 剧中
if (matchHotKey("⇧⌘C", event)) {
if (vditor.currentMode === "ir") {
setTableAlign(tableElement, "center");
execAfterRender(vditor);
event.preventDefault();
return true;
} else {
const itemElement: HTMLElement = vditor.wysiwyg.popover.querySelector('[data-type="center"]');
if (itemElement) {
itemElement.click();
event.preventDefault();
return true;
}
}
}
// 剧右
if (matchHotKey("⇧⌘R", event)) {
if (vditor.currentMode === "ir") {
setTableAlign(tableElement, "right");
execAfterRender(vditor);
event.preventDefault();
return true;
} else {
const itemElement: HTMLElement = vditor.wysiwyg.popover.querySelector('[data-type="right"]');
if (itemElement) {
itemElement.click();
event.preventDefault();
return true;
}
}
}
}
return false;
};
export const fixCodeBlock = (vditor: IVditor, event: KeyboardEvent, codeRenderElement: HTMLElement, range: Range) => {
// 行级代码块中 command + a,近对当前代码块进行全选
if (codeRenderElement.tagName === "PRE" && matchHotKey("⌘A", event)) {
range.selectNodeContents(codeRenderElement.firstElementChild);
event.preventDefault();
return true;
}
// tab
// TODO shift + tab, shift and 选中文字
if (vditor.options.tab && event.key === "Tab" && !event.shiftKey && range.toString() === "") {
range.insertNode(document.createTextNode(vditor.options.tab));
range.collapse(false);
execAfterRender(vditor);
event.preventDefault();
return true;
}
// Backspace: 光标位于第零个字符,仅删除代码块标签
if (event.key === "Backspace" && !isCtrl(event) && !event.shiftKey && !event.altKey) {
const codePosition = getSelectPosition(codeRenderElement, vditor[vditor.currentMode].element, range);
if ((codePosition.start === 0 ||
(codePosition.start === 1 && codeRenderElement.innerText === "\n")) // 空代码块,光标在 \n 后
&& range.toString() === "") {
codeRenderElement.parentElement.outerHTML =
`<p data-block="0"><wbr>${codeRenderElement.firstElementChild.innerHTML}</p>`;
setRangeByWbr(vditor[vditor.currentMode].element, range);
execAfterRender(vditor);
event.preventDefault();
return true;
}
}
// 换行
if (!isCtrl(event) && !event.altKey && event.key === "Enter") {
if (!codeRenderElement.firstElementChild.textContent.endsWith("\n")) {
codeRenderElement.firstElementChild.insertAdjacentText("beforeend", "\n");
}
range.extractContents();
range.insertNode(document.createTextNode("\n"));
range.collapse(false);
setSelectionFocus(range);
if (!isFirefox()) {
if (vditor.currentMode === "wysiwyg") {
input(vditor, range);
} else {
IRInput(vditor, range);
}
}
scrollCenter(vditor);
event.preventDefault();
return true;
}
return false;
};
export const fixBlockquote = (vditor: IVditor, range: Range, event: KeyboardEvent, pElement: HTMLElement | false) => {
const startContainer = range.startContainer;
const blockquoteElement = hasClosestByMatchTag(startContainer, "BLOCKQUOTE");
if (blockquoteElement && range.toString() === "") {
if (event.key === "Backspace" && !isCtrl(event) && !event.shiftKey && !event.altKey &&
getSelectPosition(blockquoteElement, vditor[vditor.currentMode].element, range).start === 0) {
// Backspace: 光标位于引用中的第零个字符,仅删除引用标签
range.insertNode(document.createElement("wbr"));
blockquoteElement.outerHTML = blockquoteElement.innerHTML;
setRangeByWbr(vditor[vditor.currentMode].element, range);
execAfterRender(vditor);
event.preventDefault();
return true;
}
if (pElement && event.key === "Enter" && !isCtrl(event) && !event.shiftKey && !event.altKey
&& pElement.parentElement.tagName === "BLOCKQUOTE") {
// Enter: 空行回车应逐层跳出
let isEmpty = false;
if (pElement.innerHTML.replace(Constants.ZWSP, "") === "\n" ||
pElement.innerHTML.replace(Constants.ZWSP, "") === "") {
// 空 P
isEmpty = true;
pElement.remove();
} else if (pElement.innerHTML.endsWith("\n\n") &&
getSelectPosition(pElement, vditor[vditor.currentMode].element, range).start ===
pElement.textContent.length - 1) {
// 软换行
pElement.innerHTML = pElement.innerHTML.substr(0, pElement.innerHTML.length - 2);
isEmpty = true;
}
if (isEmpty) {
// 需添加零宽字符,否则的话无法记录 undo
blockquoteElement.insertAdjacentHTML("afterend", `<p data-block="0">${Constants.ZWSP}<wbr>\n</p>`);
setRangeByWbr(vditor[vditor.currentMode].element, range);
execAfterRender(vditor);
event.preventDefault();
return true;
}
}
const blockElement = hasClosestBlock(startContainer);
if (vditor.currentMode === "wysiwyg" && blockElement && matchHotKey("⇧⌘;", event)) {
// 插入 blockquote
range.insertNode(document.createElement("wbr"));
blockElement.outerHTML = `<blockquote data-block="0">${blockElement.outerHTML}</blockquote>`;
setRangeByWbr(vditor.wysiwyg.element, range);
afterRenderEvent(vditor);
event.preventDefault();
return true;
}
if (insertAfterBlock(vditor, event, range, blockquoteElement, blockquoteElement)) {
return true;
}
if (insertBeforeBlock(vditor, event, range, blockquoteElement, blockquoteElement)) {
return true;
}
}
return false;
};
export const fixTask = (vditor: IVditor, range: Range, event: KeyboardEvent) => {
const startContainer = range.startContainer;
const taskItemElement = hasClosestByMatchTag(startContainer, "LI");
if (taskItemElement && taskItemElement.classList.contains("vditor-task")) {
if (matchHotKey("⇧⌘J", event)) {
// ctrl + shift: toggle checked
const inputElement = taskItemElement.firstElementChild as HTMLInputElement;
if (inputElement.checked) {
inputElement.removeAttribute("checked");
} else {
inputElement.setAttribute("checked", "checked");
}
execAfterRender(vditor);
event.preventDefault();
return true;
}
// Backspace: 在选择框前进行删除
if (event.key === "Backspace" && !isCtrl(event) && !event.shiftKey && !event.altKey && range.toString() === ""
&& range.startOffset === 1
&& ((startContainer.nodeType === 3 && startContainer.previousSibling &&
(startContainer.previousSibling as HTMLElement).tagName === "INPUT")
|| startContainer.nodeType !== 3)) {
const previousElement = taskItemElement.previousElementSibling;
taskItemElement.querySelector("input").remove();
if (previousElement) {
const lastNode = getLastNode(previousElement);
lastNode.parentElement.insertAdjacentHTML("beforeend", "<wbr>" + taskItemElement.innerHTML.trim());
taskItemElement.remove();
} else {
taskItemElement.parentElement.insertAdjacentHTML("beforebegin",
`<p data-block="0"><wbr>${taskItemElement.innerHTML.trim() || "\n"}</p>`);
if (taskItemElement.nextElementSibling) {
taskItemElement.remove();
} else {
taskItemElement.parentElement.remove();
}
}
setRangeByWbr(vditor[vditor.currentMode].element, range);
execAfterRender(vditor);
event.preventDefault();
return true;
}
if (event.key === "Enter" && !isCtrl(event) && !event.shiftKey && !event.altKey) {
if (taskItemElement.textContent.trim() === "") {
// 当前任务列表无文字
if (hasClosestByClassName(taskItemElement.parentElement, "vditor-task")) {
// 为子元素时,需进行反向缩进
const topListElement = getTopList(startContainer);
if (topListElement) {
listOutdent(vditor, taskItemElement, range, topListElement);
}
} else {
// 仅有一级任务列表
if (taskItemElement.nextElementSibling) {
// 任务列表下方还有元素,需要使用用段落隔断
let afterHTML = "";
let beforeHTML = "";
let isAfter = false;
Array.from(taskItemElement.parentElement.children).forEach((taskItem) => {
if (taskItemElement.isSameNode(taskItem)) {
isAfter = true;
} else {
if (isAfter) {
afterHTML += taskItem.outerHTML;
} else {
beforeHTML += taskItem.outerHTML;
}
}
});
const parentTagName = taskItemElement.parentElement.tagName;
const dataMarker = taskItemElement.parentElement.tagName === "OL" ? "" : ` data-marker="${taskItemElement.parentElement.getAttribute("data-marker")}"`;
let startAttribute = "";
if (beforeHTML) {
startAttribute = taskItemElement.parentElement.tagName === "UL" ? "" : ` start="1"`;
beforeHTML = `<${parentTagName} data-tight="true"${dataMarker} data-block="0">${beforeHTML}</${parentTagName}>`;
}
// <p data-block="0">\n<wbr></p> => <p data-block="0"><wbr>\n</p>
// https://github.com/Vanessa219/vditor/issues/430
taskItemElement.parentElement.outerHTML = `${beforeHTML}<p data-block="0"><wbr>\n</p><${parentTagName}
data-tight="true"${dataMarker} data-block="0"${startAttribute}>${afterHTML}</${parentTagName}>`;
} else {
// 任务列表下方无任务列表元素
taskItemElement.parentElement.insertAdjacentHTML("afterend", `<p data-block="0"><wbr>\n</p>`);
if (taskItemElement.parentElement.querySelectorAll("li").length === 1) {
// 任务列表仅有一项时,使用 p 元素替换
taskItemElement.parentElement.remove();
} else {
// 任务列表有多项时,当前任务列表位于最后一项,移除该任务列表
taskItemElement.remove();
}
}
}
} els