vditor
Version:
♏ 易于使用的 Markdown 编辑器,为适配不同的应用场景而生
277 lines (254 loc) • 12.2 kB
text/typescript
import {Constants} from "../constants";
import {processAfterRender} from "../ir/process";
import {code160to32} from "../util/code160to32";
import {isCtrl} from "../util/compatibility";
import {execAfterRender} from "../util/fixBrowserBehavior";
import {hasClosestByAttribute, hasClosestByClassName} from "../util/hasClosest";
import {processCodeRender} from "../util/processCode";
import {getCursorPosition, insertHTML, setSelectionFocus} from "../util/selection";
export class Hint {
public timeId: number;
public element: HTMLDivElement;
public recentLanguage: string;
private splitChar = "";
private lastIndex = -1;
constructor(hintExtends: IHintExtend[]) {
this.timeId = -1;
this.element = document.createElement("div");
this.element.className = "vditor-hint";
this.recentLanguage = "";
hintExtends.push({key: ":"});
}
public render(vditor: IVditor) {
if (!window.getSelection().focusNode) {
return;
}
let currentLineValue: string;
const range = getSelection().getRangeAt(0);
currentLineValue = range.startContainer.textContent.substring(0, range.startOffset) || "";
const key = this.getKey(currentLineValue, vditor.options.hint.extend);
if (typeof key === "undefined") {
this.element.style.display = "none";
clearTimeout(this.timeId);
} else {
if (this.splitChar === ":") {
const emojiHint = key === "" ? vditor.options.hint.emoji : vditor.lute.GetEmojis();
const matchEmojiData: IHintData[] = [];
Object.keys(emojiHint).forEach((keyName) => {
if (keyName.indexOf(key.toLowerCase()) === 0) {
if (emojiHint[keyName].indexOf(".") > -1) {
matchEmojiData.push({
html: `<img src="${emojiHint[keyName]}" title=":${keyName}:"/> :${keyName}:`,
value: `:${keyName}:`,
});
} else {
matchEmojiData.push({
html: `<span class="vditor-hint__emoji">${emojiHint[keyName]}</span>${keyName}`,
value: emojiHint[keyName],
});
}
}
});
this.genHTML(matchEmojiData, key, vditor);
} else {
vditor.options.hint.extend.forEach((item) => {
if (item.key === this.splitChar) {
clearTimeout(this.timeId);
this.timeId = window.setTimeout(async () => {
this.genHTML(await item.hint(key), key, vditor);
}, vditor.options.hint.delay);
}
});
}
}
}
public genHTML(data: IHintData[], key: string, vditor: IVditor) {
if (data.length === 0) {
this.element.style.display = "none";
return;
}
const editorElement = vditor[vditor.currentMode].element;
const textareaPosition = getCursorPosition(editorElement);
const x = textareaPosition.left +
(vditor.options.outline.position === "left" ? vditor.outline.element.offsetWidth : 0);
const y = textareaPosition.top;
let hintsHTML = "";
data.forEach((hintData, i) => {
if (i > 7) {
return;
}
// process high light
let html = hintData.html;
if (key !== "") {
const lastIndex = html.lastIndexOf(">") + 1;
let replaceHtml = html.substr(lastIndex);
const replaceIndex = replaceHtml.toLowerCase().indexOf(key.toLowerCase());
if (replaceIndex > -1) {
replaceHtml = replaceHtml.substring(0, replaceIndex) + "<b>" +
replaceHtml.substring(replaceIndex, replaceIndex + key.length) + "</b>" +
replaceHtml.substring(replaceIndex + key.length);
html = html.substr(0, lastIndex) + replaceHtml;
}
}
hintsHTML += `<button type="button" data-value="${encodeURIComponent(hintData.value)} "
${i === 0 ? "class='vditor-hint--current'" : ""}> ${html}</button>`;
});
this.element.innerHTML = hintsHTML;
const lineHeight = parseInt(document.defaultView.getComputedStyle(editorElement, null)
.getPropertyValue("line-height"), 10);
this.element.style.top = `${y + (lineHeight || 22)}px`;
this.element.style.left = `${x}px`;
this.element.style.display = "block";
this.element.style.right = "auto";
this.element.querySelectorAll("button").forEach((element) => {
element.addEventListener("click", (event) => {
this.fillEmoji(element, vditor);
event.preventDefault();
});
});
// hint 展现在上部
if (this.element.getBoundingClientRect().bottom > window.innerHeight) {
this.element.style.top = `${y - this.element.offsetHeight}px`;
}
if (this.element.getBoundingClientRect().right > window.innerWidth) {
this.element.style.left = "auto";
this.element.style.right = "0";
}
}
public fillEmoji = (element: HTMLElement, vditor: IVditor) => {
this.element.style.display = "none";
const value = decodeURIComponent(element.getAttribute("data-value"));
const range: Range = window.getSelection().getRangeAt(0);
// 代码提示
if (vditor.currentMode === "ir") {
const preBeforeElement = hasClosestByAttribute(range.startContainer, "data-type", "code-block-info");
if (preBeforeElement) {
preBeforeElement.textContent = Constants.ZWSP + value.trimRight();
range.selectNodeContents(preBeforeElement);
range.collapse(false);
processAfterRender(vditor);
preBeforeElement.parentElement.querySelectorAll("code").forEach((item) => {
item.className = "language-" + value.trimRight();
});
processCodeRender(preBeforeElement.parentElement.querySelector(".vditor-ir__preview"), vditor);
this.recentLanguage = value.trimRight();
return;
}
}
if (vditor.currentMode === "wysiwyg" && range.startContainer.nodeType !== 3 ) {
const startContainer = range.startContainer as HTMLElement;
let inputElement: HTMLInputElement;
if (startContainer.classList.contains("vditor-input")) {
inputElement = startContainer as HTMLInputElement;
} else {
inputElement = startContainer.firstElementChild as HTMLInputElement;
}
if (inputElement && inputElement.classList.contains("vditor-input")) {
inputElement.value = value.trimRight();
range.selectNodeContents(inputElement);
range.collapse(false);
// {detail: 1}用于标识这个自定义事件是在编程语言选择后触发的
// 用于在鼠标选择语言后,自动聚焦到代码输入框
inputElement.dispatchEvent(new CustomEvent("input", {detail: 1}));
this.recentLanguage = value.trimRight();
return;
}
}
range.setStart(range.startContainer, this.lastIndex);
range.deleteContents();
if (vditor.options.hint.parse) {
if (vditor.currentMode === "sv") {
insertHTML(vditor.lute.SpinVditorSVDOM(value), vditor);
} else if (vditor.currentMode === "wysiwyg") {
insertHTML(vditor.lute.SpinVditorDOM(value), vditor);
} else {
insertHTML(vditor.lute.SpinVditorIRDOM(value), vditor);
}
} else {
insertHTML(value, vditor);
}
if (this.splitChar === ":" && value.indexOf(":") > -1 && vditor.currentMode !== "sv") {
range.insertNode(document.createTextNode(" "));
}
range.collapse(false);
setSelectionFocus(range);
if (vditor.currentMode === "wysiwyg") {
const preElement = hasClosestByClassName(range.startContainer, "vditor-wysiwyg__block");
if (preElement && preElement.lastElementChild.classList.contains("vditor-wysiwyg__preview")) {
preElement.lastElementChild.innerHTML = preElement.firstElementChild.innerHTML;
processCodeRender(preElement.lastElementChild as HTMLElement, vditor);
}
} else if (vditor.currentMode === "ir") {
const preElement = hasClosestByClassName(range.startContainer, "vditor-ir__marker--pre");
if (preElement && preElement.nextElementSibling.classList.contains("vditor-ir__preview")) {
preElement.nextElementSibling.innerHTML = preElement.innerHTML;
processCodeRender(preElement.nextElementSibling as HTMLElement, vditor);
}
}
execAfterRender(vditor);
}
public select(event: KeyboardEvent, vditor: IVditor) {
if (this.element.querySelectorAll("button").length === 0 ||
this.element.style.display === "none") {
return false;
}
const currentHintElement: HTMLElement = this.element.querySelector(".vditor-hint--current");
if (event.key === "ArrowDown") {
event.preventDefault();
event.stopPropagation();
currentHintElement.removeAttribute("class");
if (!currentHintElement.nextElementSibling) {
this.element.children[0].className = "vditor-hint--current";
} else {
currentHintElement.nextElementSibling.className = "vditor-hint--current";
}
return true;
} else if (event.key === "ArrowUp") {
event.preventDefault();
event.stopPropagation();
currentHintElement.removeAttribute("class");
if (!currentHintElement.previousElementSibling) {
const length = this.element.children.length;
this.element.children[length - 1].className = "vditor-hint--current";
} else {
currentHintElement.previousElementSibling.className = "vditor-hint--current";
}
return true;
} else if (!isCtrl(event) && !event.shiftKey && !event.altKey && event.key === "Enter" && !event.isComposing) {
event.preventDefault();
event.stopPropagation();
this.fillEmoji(currentHintElement, vditor);
return true;
}
return false;
}
private getKey(currentLineValue: string, extend: IHintExtend[]) {
this.lastIndex = -1;
this.splitChar = "";
extend.forEach((item) => {
const currentLastIndex = currentLineValue.lastIndexOf(item.key);
if (this.lastIndex < currentLastIndex) {
this.splitChar = item.key;
this.lastIndex = currentLastIndex;
}
});
let key;
if (this.lastIndex === -1) {
return key;
}
const lineArray = currentLineValue.split(this.splitChar);
const lastItem = lineArray[lineArray.length - 1];
const maxLength = 32;
if (lineArray.length > 1 && lastItem.trim() === lastItem) {
if (lineArray.length === 2 && lineArray[0] === "" && lineArray[1].length < maxLength) {
key = lineArray[1];
} else {
const preChar = lineArray[lineArray.length - 2].slice(-1);
if (code160to32(preChar) === " " && lastItem.length < maxLength) {
key = lastItem;
}
}
}
return key;
}
}