feat: 新增 WYSIWYG 编辑器功能和相关工具栏事件处理

This commit is contained in:
2026-04-10 16:18:01 +08:00
parent 40b77471b5
commit f20b7aa61c
128 changed files with 16048 additions and 84 deletions

View File

@@ -0,0 +1,27 @@
import {Constants} from "../constants";
import {addScript} from "../util/addScript";
import {abcRenderAdapter} from "./adapterRender";
declare const ABCJS: {
renderAbc(element: HTMLElement, text: string): void;
};
export const abcRender = (element: (HTMLElement | Document) = document, cdn = Constants.CDN) => {
const abcElements = abcRenderAdapter.getElements(element);
if (abcElements.length > 0) {
addScript(`${cdn}/dist/js/abcjs/abcjs_basic.min.js`, "vditorAbcjsScript").then(() => {
abcElements.forEach((item: HTMLDivElement) => {
if (item.parentElement.classList.contains("vditor-wysiwyg__pre") ||
item.parentElement.classList.contains("vditor-ir__marker--pre")) {
return;
}
if (item.getAttribute("data-processed") === "true") {
return;
}
ABCJS.renderAbc(item, abcRenderAdapter.getCode(item).trim());
item.style.overflowX = "auto";
item.setAttribute("data-processed", "true");
});
});
}
};

View File

@@ -0,0 +1,37 @@
export const mathRenderAdapter = {
getCode: (mathElement: Element) => mathElement.textContent,
getElements: (element: HTMLElement) => element.querySelectorAll(".language-math"),
};
export const mermaidRenderAdapter = {
/** 不仅要返回code并且需要将 code 设置为 el 的 innerHTML */
getCode: (el: Element) => el.textContent,
getElements: (element: HTMLElement) => element.querySelectorAll(".language-mermaid"),
};
export const markmapRenderAdapter = {
getCode: (el: Element) => el.textContent,
getElements: (element: HTMLElement) => element.querySelectorAll(".language-markmap"),
};
export const mindmapRenderAdapter = {
getCode: (el: Element) => el.getAttribute("data-code"),
getElements: (el: HTMLElement | Document) => el.querySelectorAll(".language-mindmap"),
};
export const chartRenderAdapter = {
getCode: (el: HTMLElement) => el.innerText,
getElements: (el: HTMLElement | Document) => el.querySelectorAll(".language-echarts"),
};
export const abcRenderAdapter = {
getCode: (el: Element) => el.textContent,
getElements: (el: HTMLElement | Document) => el.querySelectorAll(".language-abc"),
};
export const graphvizRenderAdapter = {
getCode: (el: Element) => el.textContent,
getElements: (el: HTMLElement | Document) => el.querySelectorAll(".language-graphviz"),
};
export const flowchartRenderAdapter = {
getCode: (el: Element) => el.textContent,
getElements: (el: HTMLElement | Document) => el.querySelectorAll(".language-flowchart"),
};
export const plantumlRenderAdapter = {
getCode: (el: Element) => el.textContent,
getElements: (el: HTMLElement | Document) => el.querySelectorAll(".language-plantuml"),
};

View File

@@ -0,0 +1,19 @@
export const anchorRender = (type: number) => {
document.querySelectorAll(".vditor-anchor").forEach((anchor: HTMLLinkElement) => {
if (type === 1) {
anchor.classList.add("vditor-anchor--left");
}
anchor.onclick = () => {
const id = anchor.getAttribute("href").substr(1);
const top = document.getElementById("vditorAnchor-" + id).offsetTop;
document.querySelector("html").scrollTop = top;
};
});
window.onhashchange = () => {
const element = document.getElementById("vditorAnchor-" + decodeURIComponent(window.location.hash.substr(1)));
if (element) {
document.querySelector("html").scrollTop = element.offsetTop;
}
};
};

View File

@@ -0,0 +1,38 @@
import {Constants} from "../constants";
import {addScript} from "../util/addScript";
import {chartRenderAdapter} from "./adapterRender";
import {looseJsonParse} from "../util/function";
declare const echarts: {
init(element: HTMLElement, theme?: string): IEChart;
};
export const chartRender = (element: (HTMLElement | Document) = document, cdn = Constants.CDN, theme: string) => {
const echartsElements = chartRenderAdapter.getElements(element);
if (echartsElements.length > 0) {
addScript(`${cdn}/dist/js/echarts/echarts.min.js?v=5.5.1`, "vditorEchartsScript").then(() => {
echartsElements.forEach(async (e: HTMLDivElement) => {
if (e.parentElement.classList.contains("vditor-wysiwyg__pre") ||
e.parentElement.classList.contains("vditor-ir__marker--pre")) {
return;
}
const text = chartRenderAdapter.getCode(e).trim();
if (!text) {
return;
}
try {
if (e.getAttribute("data-processed") === "true") {
return;
}
const option = await looseJsonParse(text);
echarts.init(e, theme === "dark" ? "dark" : undefined).setOption(option);
e.setAttribute("data-processed", "true");
} catch (error) {
e.className = "vditor-reset--error";
e.innerHTML = `echarts render error: <br>${error}`;
}
});
});
}
};

View File

@@ -0,0 +1,60 @@
import {code160to32} from "../util/code160to32";
import {Constants} from "../constants";
export const codeRender = (element: HTMLElement, option?: IHljs) => {
Array.from<HTMLElement>(element.querySelectorAll("pre > code")).filter((e, index) => {
if (e.parentElement.classList.contains("vditor-wysiwyg__pre") ||
e.parentElement.classList.contains("vditor-ir__marker--pre")) {
return false;
}
if (e.classList.contains("language-mermaid") || e.classList.contains("language-flowchart") ||
e.classList.contains("language-echarts") || e.classList.contains("language-mindmap") ||
e.classList.contains("language-plantuml") || e.classList.contains("language-markmap") ||
e.classList.contains("language-abc") || e.classList.contains("language-graphviz") ||
e.classList.contains("language-math")) {
return false;
}
if (e.style.maxHeight.indexOf("px") > -1) {
return false;
}
// 避免预览区在渲染后由于代码块过多产生性能问题 https://github.com/b3log/vditor/issues/67
if (element.classList.contains("vditor-preview") && index > 5) {
return false;
}
return true;
}).forEach((e) => {
let codeText = e.innerText;
if (e.classList.contains("highlight-chroma")) {
const codeElement = e.cloneNode(true) as HTMLElement;
codeElement.querySelectorAll(".highlight-ln").forEach((item: HTMLElement) => {
item.remove();
});
codeText = codeElement.innerText;
} else if (codeText.endsWith("\n")) {
codeText = codeText.substr(0, codeText.length - 1)
}
let iconHTML = '<svg><use xlink:href="#vditor-icon-copy"></use></svg>';
if (!document.getElementById("vditorIconScript")) {
iconHTML = '<svg viewBox="0 0 32 32"><path d="M22.545-0h-17.455c-1.6 0-2.909 1.309-2.909 2.909v20.364h2.909v-20.364h17.455v-2.909zM26.909 5.818h-16c-1.6 0-2.909 1.309-2.909 2.909v20.364c0 1.6 1.309 2.909 2.909 2.909h16c1.6 0 2.909-1.309 2.909-2.909v-20.364c0-1.6-1.309-2.909-2.909-2.909zM26.909 29.091h-16v-20.364h16v20.364z"></path></svg>';
}
const divElement = document.createElement("div");
divElement.className = "vditor-copy";
divElement.innerHTML = `<span aria-label="${window.VditorI18n?.copy || "复制"}"
onmouseover="this.setAttribute('aria-label', '${window.VditorI18n?.copy || "复制"}')"
class="vditor-tooltipped vditor-tooltipped__w"
onclick="this.previousElementSibling.select();document.execCommand('copy');this.setAttribute('aria-label', '${window.VditorI18n?.copied || "已复制"}');this.previousElementSibling.blur()">${iconHTML}</span>`;
const textarea = document.createElement("textarea");
textarea.value = code160to32(codeText);
divElement.insertAdjacentElement("afterbegin", textarea);
if (option && option.renderMenu) {
option.renderMenu(e, divElement);
}
e.before(divElement);
e.style.maxHeight = (window.outerHeight - 40) + "px";
// https://github.com/Vanessa219/vditor/issues/1356
e.insertAdjacentHTML("afterend", `<span style="position: absolute">${Constants.ZWSP}</span>`)
});
};

View File

@@ -0,0 +1,25 @@
import {Constants} from "../constants";
import {addScript} from "../util/addScript";
import {flowchartRenderAdapter} from "./adapterRender";
declare const flowchart: {
parse(text: string): { drawSVG: (type: HTMLElement) => void };
};
export const flowchartRender = (element: HTMLElement, cdn = Constants.CDN) => {
const flowchartElements = flowchartRenderAdapter.getElements(element);
if (flowchartElements.length === 0) {
return;
}
addScript(`${cdn}/dist/js/flowchart.js/flowchart.min.js`, "vditorFlowchartScript").then(() => {
flowchartElements.forEach((item: HTMLElement) => {
if (item.getAttribute("data-processed") === "true") {
return;
}
const flowchartObj = flowchart.parse(flowchartRenderAdapter.getCode(item));
item.innerHTML = "";
flowchartObj.drawSVG(item);
item.setAttribute("data-processed", "true");
});
});
};

View File

@@ -0,0 +1,11 @@
import {getMarkdown} from "./getMarkdown";
export const getHTML = (vditor: IVditor) => {
if (vditor.currentMode === "sv") {
return vditor.lute.Md2HTML(getMarkdown(vditor));
} else if (vditor.currentMode === "wysiwyg") {
return vditor.lute.VditorDOM2HTML(vditor.wysiwyg.element.innerHTML);
} else if (vditor.currentMode === "ir") {
return vditor.lute.VditorIRDOM2HTML(vditor.ir.element.innerHTML);
}
};

View File

@@ -0,0 +1,12 @@
import {code160to32} from "../util/code160to32";
export const getMarkdown = (vditor: IVditor) => {
if (vditor.currentMode === "sv") {
return code160to32(`${vditor.sv.element.textContent}\n`.replace(/\n\n$/, "\n"));
} else if (vditor.currentMode === "wysiwyg") {
return vditor.lute.VditorDOM2Md(vditor.wysiwyg.element.innerHTML);
} else if (vditor.currentMode === "ir") {
return vditor.lute.VditorIRDOM2Md(vditor.ir.element.innerHTML);
}
return "";
};

View File

@@ -0,0 +1,49 @@
import {Constants} from "../constants";
import {addScript} from "../util/addScript";
import {graphvizRenderAdapter} from "./adapterRender";
declare class Viz {
public renderSVGElement: (code: string) => Promise<any>;
constructor({ }: { worker: Worker });
}
export const graphvizRender = (element: HTMLElement, cdn = Constants.CDN) => {
const graphvizElements = graphvizRenderAdapter.getElements(element);
if (graphvizElements.length === 0) {
return;
}
addScript(`${cdn}/dist/js/graphviz/viz.js`, "vditorGraphVizScript").then(() => {
graphvizElements.forEach((e: HTMLDivElement) => {
const code = graphvizRenderAdapter.getCode(e);
if (e.parentElement.classList.contains("vditor-wysiwyg__pre") ||
e.parentElement.classList.contains("vditor-ir__marker--pre")) {
return;
}
if (e.getAttribute("data-processed") === "true" || code.trim() === "") {
return;
}
try {
const blob = new Blob([`importScripts('${(document.getElementById("vditorGraphVizScript") as HTMLScriptElement).src.replace("viz.js", "full.render.js")}');`],
{ type: "application/javascript" });
const url = window.URL || window.webkitURL;
const blobUrl = url.createObjectURL(blob);
const worker = new Worker(blobUrl);
new Viz({ worker })
.renderSVGElement(code).then((result: HTMLElement) => {
e.innerHTML = result.outerHTML;
}).catch((error) => {
e.innerHTML = `graphviz render error: <br>${error}`;
e.className = "vditor-reset--error";
});
} catch (e) {
console.error("graphviz error", e);
}
e.setAttribute("data-processed", "true");
});
});
};

View File

@@ -0,0 +1,90 @@
import {Constants} from "../constants";
import {addScript} from "../util/addScript";
import {addStyle} from "../util/addStyle";
declare const hljs: {
highlightElement(element: Element): void;
};
export const highlightRender = (hljsOption?: IHljs, element: HTMLElement | Document = document,
cdn = Constants.CDN) => {
let style = hljsOption.style;
if (!Constants.CODE_THEME.includes(style)) {
style = "github";
}
const vditorHljsStyle = document.getElementById("vditorHljsStyle") as HTMLLinkElement;
const href = `${cdn}/dist/js/highlight.js/styles/${style}.css`;
if (vditorHljsStyle && vditorHljsStyle.getAttribute('href') !== href) {
vditorHljsStyle.remove();
}
addStyle(`${cdn}/dist/js/highlight.js/styles/${style}.css`, "vditorHljsStyle");
if (hljsOption.enable === false) {
return;
}
const codes = element.querySelectorAll("pre > code");
if (codes.length === 0) {
return;
}
addScript(`${cdn}/dist/js/highlight.js/highlight.pack.js`, "vditorHljsScript").then(() => {
addScript(`${cdn}/dist/js/highlight.js/solidity.min.js`, "vditorHljsSolidityScript").then(() => {
addScript(`${cdn}/dist/js/highlight.js/yul.min.js`, "vditorHljsYulScript").then(() => {
element.querySelectorAll("pre > code").forEach((block) => {
// ir & wysiwyg 区域不渲染
if (block.parentElement.classList.contains("vditor-ir__marker--pre") ||
block.parentElement.classList.contains("vditor-wysiwyg__pre")) {
return;
}
if (block.classList.contains("language-mermaid") || block.classList.contains("language-flowchart") ||
block.classList.contains("language-echarts") || block.classList.contains("language-mindmap") ||
block.classList.contains("language-plantuml") ||
block.classList.contains("language-abc") || block.classList.contains("language-graphviz") ||
block.classList.contains("language-math")) {
return;
}
if (hljsOption.defaultLang !== "" && block.className.indexOf("language-") === -1) {
block.classList.add("language-" + hljsOption.defaultLang)
}
hljs.highlightElement(block);
if (!hljsOption.lineNumber) {
return;
}
block.classList.add("vditor-linenumber");
let linenNumberTemp: HTMLDivElement = block.querySelector(".vditor-linenumber__temp");
if (!linenNumberTemp) {
linenNumberTemp = document.createElement("div");
linenNumberTemp.className = "vditor-linenumber__temp";
block.insertAdjacentElement("beforeend", linenNumberTemp);
}
const whiteSpace = getComputedStyle(block).whiteSpace;
let isSoftWrap = false;
if (whiteSpace === "pre-wrap" || whiteSpace === "pre-line") {
isSoftWrap = true;
}
let lineNumberHTML = "";
const lineList = block.textContent.split(/\r\n|\r|\n/g);
lineList.pop();
lineList.map((line) => {
let lineHeight = "";
if (isSoftWrap) {
linenNumberTemp.textContent = line || "\n";
lineHeight = ` style="height:${linenNumberTemp.getBoundingClientRect().height}px"`;
}
lineNumberHTML += `<span${lineHeight}></span>`;
});
linenNumberTemp.style.display = "none";
lineNumberHTML = `<span class="vditor-linenumber__rows">${lineNumberHTML}</span>`;
block.insertAdjacentHTML("beforeend", lineNumberHTML);
});
})
})
});
};

View File

@@ -0,0 +1,56 @@
declare global {
interface Window {
vditorImageIntersectionObserver: IntersectionObserver;
}
}
export const lazyLoadImageRender = (element: (HTMLElement | Document) = document) => {
const loadImg = (it: HTMLImageElement) => {
const testImage = document.createElement("img");
testImage.src = it.getAttribute("data-src");
testImage.addEventListener("load", () => {
if (!it.getAttribute("style") && !it.getAttribute("class") &&
!it.getAttribute("width") && !it.getAttribute("height")) {
if (testImage.naturalHeight > testImage.naturalWidth &&
testImage.naturalWidth / testImage.naturalHeight <
document.querySelector(".vditor-reset").clientWidth / (window.innerHeight - 40) &&
testImage.naturalHeight > (window.innerHeight - 40)) {
it.style.height = (window.innerHeight - 40) + "px";
}
}
it.src = testImage.src;
});
it.removeAttribute("data-src");
};
if (!("IntersectionObserver" in window)) {
element.querySelectorAll("img").forEach((imgElement: HTMLImageElement) => {
if (imgElement.getAttribute("data-src")) {
loadImg(imgElement);
}
});
return false;
}
if (window.vditorImageIntersectionObserver) {
window.vditorImageIntersectionObserver.disconnect();
element.querySelectorAll("img").forEach((imgElement) => {
window.vditorImageIntersectionObserver.observe(imgElement);
});
} else {
window.vditorImageIntersectionObserver = new IntersectionObserver((entries) => {
entries.forEach((entrie: IntersectionObserverEntry & { target: HTMLImageElement }) => {
if ((typeof entrie.isIntersecting === "undefined"
? entrie.intersectionRatio !== 0
: entrie.isIntersecting)
&& entrie.target.getAttribute("data-src")) {
loadImg(entrie.target);
}
});
});
element.querySelectorAll("img").forEach((imgElement) => {
window.vditorImageIntersectionObserver.observe(imgElement);
});
}
};

View File

@@ -0,0 +1,57 @@
import {Constants} from "../constants";
import {addScript} from "../util/addScript";
import {markmapRenderAdapter} from "./adapterRender";
declare const window: any;
const enabled: Record<string, boolean> = {};
const transform = (transformer: any,content: string)=>{
const result = transformer.transform(content);
const keys = Object.keys(result.features).filter((key) => !enabled[key]);
keys.forEach((key) => {
enabled[key] = true;
});
const { styles, scripts } = transformer.getAssets(keys);
const { markmap } = window;
if (styles) markmap.loadCSS(styles);
if (scripts) markmap.loadJS(scripts);
return result;
}
const init = (el: HTMLElement,code: string) => {
const { Transformer, Markmap, deriveOptions , globalCSS} = window.markmap;
const transformer = new Transformer();
el.innerHTML = '<svg style="width:100%"></svg>';
const svg = el.firstChild as SVGElement;
const mm = Markmap.create(svg, null);
const { root, frontmatter } = transform(transformer, code);
const markmapOptions = frontmatter?.markmap;
const frontmatterOptions = deriveOptions(markmapOptions);
mm.setData(root, frontmatterOptions);
mm.fit();
}
export const markmapRender = (element: HTMLElement, cdn = Constants.CDN, theme: string) => {
const markmapElements = markmapRenderAdapter.getElements(element);
if (markmapElements.length === 0) {
return;
}
addScript(`${cdn}/dist/js/markmap/markmap.min.js`, "vditorMermaidScript").then(() => {
markmapElements.forEach((item) => {
const code = markmapRenderAdapter.getCode(item);
if (item.getAttribute("data-processed") === "true" || code.trim() === "") {
return;
}
const render = document.createElement("div")
render.className = "language-markmap"
item.parentNode.appendChild(render)
init(render,code)
if(item.parentNode.childNodes[0].nodeName == "CODE"){
item.parentNode.removeChild(item.parentNode.childNodes[0])
}
});
});
};

View File

@@ -0,0 +1,148 @@
import {Constants} from "../constants";
import {addScript, addScriptSync} from "../util/addScript";
import {addStyle} from "../util/addStyle";
import {code160to32} from "../util/code160to32";
import {mathRenderAdapter} from "./adapterRender";
declare const katex: {
renderToString(math: string, option: {
displayMode: boolean;
output: string;
macros: object;
}): string;
};
declare global {
interface Window {
MathJax: any;
}
}
export const mathRender = (element: HTMLElement, options?: { cdn?: string, math?: IMath }) => {
const mathElements = mathRenderAdapter.getElements(element);
if (mathElements.length === 0) {
return;
}
const defaultOptions = {
cdn: Constants.CDN,
math: {
engine: "KaTeX",
inlineDigit: false,
macros: {},
},
};
if (options && options.math) {
options.math =
Object.assign({}, defaultOptions.math, options.math);
}
options = Object.assign({}, defaultOptions, options);
if (options.math.engine === "KaTeX") {
addStyle(`${options.cdn}/dist/js/katex/katex.min.css?v=0.16.9`, "vditorKatexStyle");
addScript(`${options.cdn}/dist/js/katex/katex.min.js?v=0.16.9`, "vditorKatexScript").then(() => {
addScript(`${options.cdn}/dist/js/katex/mhchem.min.js?v=0.16.9`, "vditorKatexChemScript").then(() => {
mathElements.forEach((mathElement) => {
if (mathElement.parentElement.classList.contains("vditor-wysiwyg__pre") ||
mathElement.parentElement.classList.contains("vditor-ir__marker--pre")) {
return;
}
if (mathElement.getAttribute("data-math")) {
return;
}
const math = code160to32(mathRenderAdapter.getCode(mathElement));
mathElement.setAttribute("data-math", math);
try {
mathElement.innerHTML = katex.renderToString(math, {
displayMode: mathElement.tagName === "DIV",
output: "html",
macros: options.math.macros,
});
} catch (e) {
mathElement.innerHTML = e.message;
mathElement.className = "language-math vditor-reset--error";
}
mathElement.addEventListener("copy", (event: ClipboardEvent) => {
event.stopPropagation();
event.preventDefault();
const vditorMathElement = (event.currentTarget as HTMLElement).closest(".language-math");
event.clipboardData.setData("text/html", vditorMathElement.innerHTML);
event.clipboardData.setData("text/plain",
vditorMathElement.getAttribute("data-math"));
});
});
});
});
} else if (options.math.engine === "MathJax") {
const chainAsync = (fns: any) => {
if (fns.length === 0) {
return;
}
let curr = 0;
const last = fns[fns.length - 1];
const next = () => {
const fn = fns[curr++];
fn === last ? fn() : fn(next);
};
next();
};
if (!window.MathJax) {
window.MathJax = {
loader: {
paths: {mathjax: `${options.cdn}/dist/js/mathjax`},
},
startup: {
typeset: false,
},
tex: {
macros: options.math.macros,
},
};
// https://github.com/Vanessa219/vditor/issues/1453
Object.assign(window.MathJax, options.math.mathJaxOptions);
}
// 循环加载会抛异常
addScriptSync(`${options.cdn}/dist/js/mathjax/tex-svg-full.js`, "protyleMathJaxScript");
const renderMath = (mathElement: Element, next?: () => void) => {
const math = code160to32(mathElement.textContent).trim();
const mathOptions = window.MathJax.getMetricsFor(mathElement);
mathOptions.display = mathElement.tagName === "DIV";
window.MathJax.tex2svgPromise(math, mathOptions).then((node: Element) => {
mathElement.innerHTML = "";
mathElement.setAttribute("data-math", math);
mathElement.append(node);
window.MathJax.startup.document.clear();
window.MathJax.startup.document.updateDocument();
const errorTextElement = node.querySelector('[data-mml-node="merror"]');
if (errorTextElement && errorTextElement.textContent.trim() !== "") {
mathElement.innerHTML = errorTextElement.textContent.trim();
mathElement.className = "vditor-reset--error";
}
if (next) {
next();
}
});
};
window.MathJax.startup.promise.then(() => {
const chains: any[] = [];
for (let i = 0; i < mathElements.length; i++) {
const mathElement = mathElements[i];
if (!mathElement.parentElement.classList.contains("vditor-wysiwyg__pre") &&
!mathElement.parentElement.classList.contains("vditor-ir__marker--pre") &&
!mathElement.getAttribute("data-math") && code160to32(mathElement.textContent).trim()) {
chains.push((next: () => void) => {
if (i === mathElements.length - 1) {
renderMath(mathElement);
} else {
renderMath(mathElement, next);
}
});
}
}
chainAsync(chains);
});
}
};

View File

@@ -0,0 +1,105 @@
import {getSearch} from "../util/function";
const videoRender = (element: HTMLElement, url: string) => {
element.insertAdjacentHTML("afterend", `<video controls="controls" src="${url}"></video>`);
element.remove();
};
const audioRender = (element: HTMLElement, url: string) => {
element.insertAdjacentHTML("afterend", `<audio controls="controls" src="${url}"></audio>`);
element.remove();
};
const iframeRender = (element: HTMLElement, url: string) => {
const youtubeMatch = url.match(/\/\/(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))([\w|-]{11})(?:(?:[\?&]t=)(\S+))?/);
const youkuMatch = url.match(/\/\/v\.youku\.com\/v_show\/id_(\w+)=*\.html/);
const qqMatch = url.match(/\/\/v\.qq\.com\/x\/cover\/.*\/([^\/]+)\.html\??.*/);
const coubMatch = url.match(/(?:www\.|\/\/)coub\.com\/view\/(\w+)/);
const facebookMatch = url.match(/(?:www\.|\/\/)facebook\.com\/([^\/]+)\/videos\/([0-9]+)/);
const dailymotionMatch = url.match(/.+dailymotion.com\/(video|hub)\/(\w+)\?/);
const bilibiliMatch = url.match(/(?:www\.|\/\/)bilibili\.com\/video\/(\w+)/);
const tedMatch = url.match(/(?:www\.|\/\/)ted\.com\/talks\/(\w+)/);
if (youtubeMatch && youtubeMatch[1].length === 11) {
element.insertAdjacentHTML("afterend",
`<iframe class="iframe__video" src="//www.youtube.com/embed/${youtubeMatch[1] +
(youtubeMatch[2] ? "?start=" + youtubeMatch[2] : "")}"></iframe>`);
element.remove();
} else if (youkuMatch && youkuMatch[1]) {
element.insertAdjacentHTML("afterend",
`<iframe class="iframe__video" src="//player.youku.com/embed/${youkuMatch[1]}"></iframe>`);
element.remove();
} else if (qqMatch && qqMatch[1]) {
element.insertAdjacentHTML("afterend",
`<iframe class="iframe__video" src="https://v.qq.com/txp/iframe/player.html?vid=${qqMatch[1]}"></iframe>`);
element.remove();
} else if (coubMatch && coubMatch[1]) {
element.insertAdjacentHTML("afterend",
`<iframe class="iframe__video"
src="//coub.com/embed/${coubMatch[1]}?muted=false&autostart=false&originalSize=true&startWithHD=true"></iframe>`);
element.remove();
} else if (facebookMatch && facebookMatch[0]) {
element.insertAdjacentHTML("afterend",
`<iframe class="iframe__video"
src="https://www.facebook.com/plugins/video.php?href=${encodeURIComponent(facebookMatch[0])}"></iframe>`);
element.remove();
} else if (dailymotionMatch && dailymotionMatch[2]) {
element.insertAdjacentHTML("afterend",
`<iframe class="iframe__video"
src="https://www.dailymotion.com/embed/video/${dailymotionMatch[2]}"></iframe>`);
element.remove();
} else if (url.indexOf("bilibili.com") > -1 && (url.indexOf("bvid=") > -1 || (bilibiliMatch && bilibiliMatch[1]))) {
const params: IObject = {
bvid: getSearch("bvid", url) || (bilibiliMatch && bilibiliMatch[1]),
page: "1",
high_quality: "1",
as_wide: "1",
allowfullscreen: "true",
autoplay: "0"
};
new URL(url.startsWith("http") ? url : "https:" + url).search.split("&").forEach((item, index) => {
if (!item) {
return;
}
if (index === 0) {
item = item.substr(1);
}
const keyValue = item.split("=");
params[keyValue[0]] = keyValue[1];
});
let src = "https://player.bilibili.com/player.html?";
const keys = Object.keys(params);
keys.forEach((key, index) => {
src += `${key}=${params[key]}`;
if (index < keys.length - 1) {
src += "&";
}
});
element.insertAdjacentHTML("afterend",
`<iframe class="iframe__video" src="${src}"></iframe>`);
element.remove();
} else if (tedMatch && tedMatch[1]) {
element.insertAdjacentHTML("afterend",
`<iframe class="iframe__video" src="//embed.ted.com/talks/${tedMatch[1]}"></iframe>`);
element.remove();
}
};
export const mediaRender = (element: HTMLElement) => {
if (!element) {
return;
}
element.querySelectorAll("a").forEach((aElement) => {
const url = aElement.getAttribute("href");
if (!url) {
return;
}
if (url.match(/^.+.(mp4|m4v|ogg|ogv|webm)$/)) {
videoRender(aElement, url);
} else if (url.match(/^.+.(mp3|wav|flac)$/)) {
audioRender(aElement, url);
} else {
iframeRender(aElement, url);
}
});
};

View File

@@ -0,0 +1,60 @@
import {Constants} from "../constants";
import {addScript} from "../util/addScript";
import {mermaidRenderAdapter} from "./adapterRender";
import {genUUID} from "../util/function";
declare const mermaid: {
initialize(options: any): void,
render(id: string, text: string): { svg: string }
};
export const mermaidRender = (element: HTMLElement, cdn = Constants.CDN, theme: string) => {
const mermaidElements = mermaidRenderAdapter.getElements(element);
if (mermaidElements.length === 0) {
return;
}
addScript(`${cdn}/dist/js/mermaid/mermaid.min.js`, "vditorMermaidScript").then(() => {
const config: any = {
securityLevel: "loose", // 升级后无 https://github.com/siyuan-note/siyuan/issues/3587可使用该选项
altFontFamily: "sans-serif",
fontFamily: "sans-serif",
startOnLoad: false,
flowchart: {
htmlLabels: true,
useMaxWidth: !0
},
sequence: {
useMaxWidth: true,
diagramMarginX: 8,
diagramMarginY: 8,
boxMargin: 8,
showSequenceNumbers: true // Mermaid 时序图增加序号 https://github.com/siyuan-note/siyuan/pull/6992 https://mermaid.js.org/syntax/sequenceDiagram.html#sequencenumbers
},
gantt: {
leftPadding: 75,
rightPadding: 20
}
};
if (theme === "dark") {
config.theme = "dark";
}
mermaid.initialize(config);
mermaidElements.forEach(async (item) => {
const code = mermaidRenderAdapter.getCode(item);
if (item.getAttribute("data-processed") === "true" || code.trim() === "") {
return;
}
const id = "mermaid"+genUUID()
try {
const mermaidData = await mermaid.render(id, item.textContent);
item.innerHTML = mermaidData.svg;
} catch (e) {
const errorElement = document.querySelector("#" + id);
item.innerHTML = `${errorElement.outerHTML}<br>
<div style="text-align: left"><small>${e.message.replace(/\n/, "<br>")}</small></div>`;
errorElement.parentElement.remove();
}
item.setAttribute("data-processed", "true");
});
});
};

View File

@@ -0,0 +1,74 @@
import {Constants} from "../constants";
import {addScript} from "../util/addScript";
import {mindmapRenderAdapter} from "./adapterRender";
declare const echarts: {
init(element: HTMLElement, theme?: string): IEChart;
};
export const mindmapRender = (element: (HTMLElement | Document) = document, cdn = Constants.CDN, theme: string) => {
const mindmapElements = mindmapRenderAdapter.getElements(element);
if (mindmapElements.length > 0) {
addScript(`${cdn}/dist/js/echarts/echarts.min.js?v=5.5.1`, "vditorEchartsScript").then(() => {
mindmapElements.forEach((e: HTMLDivElement) => {
if (e.parentElement.classList.contains("vditor-wysiwyg__pre") ||
e.parentElement.classList.contains("vditor-ir__marker--pre")) {
return;
}
const text = mindmapRenderAdapter.getCode(e);
if (!text) {
return;
}
try {
if (e.getAttribute("data-processed") === "true") {
return;
}
echarts.init(e, theme === "dark" ? "dark" : undefined).setOption({
series: [
{
data: [JSON.parse(decodeURIComponent(text))],
initialTreeDepth: -1,
itemStyle: {
borderWidth: 0,
color: "#4285f4",
},
label: {
backgroundColor: "#f6f8fa",
borderColor: "#d1d5da",
borderRadius: 5,
borderWidth: 0.5,
color: "#586069",
lineHeight: 20,
offset: [-5, 0],
padding: [0, 5],
position: "insideRight",
},
lineStyle: {
color: "#d1d5da",
width: 1,
},
roam: true,
symbol: (value: number, params: { data?: { children?: object } }) => {
if (params?.data?.children) {
return "circle";
} else {
return "path://";
}
},
type: "tree",
},
],
tooltip: {
trigger: "item",
triggerOn: "mousemove",
},
});
e.setAttribute("data-processed", "true");
} catch (error) {
e.className = "vditor-reset--error";
e.innerHTML = `mindmap render error: <br>${error}`;
}
});
});
}
};

View File

@@ -0,0 +1,110 @@
import {hasClosestByHeadings} from "../util/hasClosestByHeadings";
import {mathRender} from "./mathRender";
export const outlineRender = (contentElement: HTMLElement, targetElement: Element, vditor?: IVditor) => {
let tocHTML = "";
const ids: string[] = [];
Array.from(contentElement.children).forEach((item: HTMLElement, index: number) => {
if (hasClosestByHeadings(item)) {
if (vditor) {
const lastIndex = item.id.lastIndexOf("_");
item.id = item.id.substring(0, lastIndex === -1 ? undefined : lastIndex) + "_" + index;
}
ids.push(item.id);
tocHTML += item.outerHTML.replace("<wbr>", "");
}
});
if (tocHTML === "") {
targetElement.innerHTML = "";
return "";
}
const tempElement = document.createElement("div");
if (vditor) {
vditor.lute.SetToC(true);
if (vditor.currentMode === "wysiwyg" && !vditor.preview.element.contains(contentElement)) {
tempElement.innerHTML = vditor.lute.SpinVditorDOM("<p>[ToC]</p>" + tocHTML);
} else if (vditor.currentMode === "ir" && !vditor.preview.element.contains(contentElement)) {
tempElement.innerHTML = vditor.lute.SpinVditorIRDOM("<p>[ToC]</p>" + tocHTML);
} else {
tempElement.innerHTML = vditor.lute.HTML2VditorDOM("<p>[ToC]</p>" + tocHTML);
}
vditor.lute.SetToC(vditor.options.preview.markdown.toc);
} else {
targetElement.classList.add("vditor-outline");
const lute = Lute.New();
lute.SetToC(true);
tempElement.innerHTML = lute.HTML2VditorDOM("<p>[ToC]</p>" + tocHTML);
}
const headingsElement = tempElement.firstElementChild.querySelectorAll("li > span[data-target-id]");
headingsElement.forEach((item, index) => {
if (item.nextElementSibling && item.nextElementSibling.tagName === "UL") {
let iconHTML = "<svg class='vditor-outline__action'><use xlink:href='#vditor-icon-down'></use></svg>";
if (!document.getElementById("vditorIconScript")) {
iconHTML = '<svg class="vditor-outline__action" viewBox="0 0 32 32"><path d="M3.76 6.12l12.24 12.213 12.24-12.213 3.76 3.76-16 16-16-16 3.76-3.76z"></path></svg>';
}
item.innerHTML = `${iconHTML}<span>${item.innerHTML}</span>`;
} else {
item.innerHTML = `<svg></svg><span>${item.innerHTML}</span>`;
}
item.setAttribute("data-target-id", ids[index]);
});
tocHTML = tempElement.firstElementChild.innerHTML;
if (headingsElement.length === 0) {
targetElement.innerHTML = "";
return tocHTML;
}
targetElement.innerHTML = tocHTML;
if (vditor) {
mathRender(targetElement as HTMLElement, {
cdn: vditor.options.cdn,
math: vditor.options.preview.math,
});
}
targetElement.firstElementChild.addEventListener("click", (event: Event) => {
let target = event.target as HTMLElement;
while (target && !target.isEqualNode(targetElement)) {
if (target.classList.contains("vditor-outline__action")) {
if (target.classList.contains("vditor-outline__action--close")) {
target.classList.remove("vditor-outline__action--close");
target.parentElement.nextElementSibling.setAttribute("style", "display:block");
} else {
target.classList.add("vditor-outline__action--close");
target.parentElement.nextElementSibling.setAttribute("style", "display:none");
}
event.preventDefault();
event.stopPropagation();
break;
} else if (target.getAttribute("data-target-id")) {
event.preventDefault();
event.stopPropagation();
const idElement = document.getElementById(target.getAttribute("data-target-id"));
if (!idElement) {
return;
}
if (vditor) {
if (vditor.options.height === "auto") {
let windowScrollY = idElement.offsetTop + vditor.element.offsetTop;
if (!vditor.options.toolbarConfig.pin) {
windowScrollY += vditor.toolbar.element.offsetHeight;
}
window.scrollTo(window.scrollX, windowScrollY);
} else {
if (vditor.element.offsetTop < window.scrollY) {
window.scrollTo(window.scrollX, vditor.element.offsetTop);
}
if (vditor.preview.element.contains(contentElement)) {
contentElement.parentElement.scrollTop = idElement.offsetTop;
} else {
contentElement.scrollTop = idElement.offsetTop;
}
}
} else {
window.scrollTo(window.scrollX, idElement.offsetTop);
}
break;
}
target = target.parentElement;
}
});
return tocHTML;
};

View File

@@ -0,0 +1,32 @@
import {Constants} from "../constants";
import {addScript} from "../util/addScript";
import {plantumlRenderAdapter} from "./adapterRender";
declare const plantumlEncoder: {
encode(options: string): string,
};
export const plantumlRender = (element: (HTMLElement | Document) = document, cdn = Constants.CDN) => {
const plantumlElements = plantumlRenderAdapter.getElements(element);
if (plantumlElements.length === 0) {
return;
}
addScript(`${cdn}/dist/js/plantuml/plantuml-encoder.min.js`, "vditorPlantumlScript").then(() => {
plantumlElements.forEach((e: HTMLDivElement) => {
if (e.parentElement.classList.contains("vditor-wysiwyg__pre") ||
e.parentElement.classList.contains("vditor-ir__marker--pre")) {
return;
}
const text = plantumlRenderAdapter.getCode(e).trim();
if (!text) {
return;
}
try {
e.innerHTML = `<object type="image/svg+xml" data="https://www.plantuml.com/plantuml/svg/~1${plantumlEncoder.encode(text)}"/>`;
} catch (error) {
e.className = "vditor-reset--error";
e.innerHTML = `plantuml render error: <br>${error}`;
}
});
});
};

View File

@@ -0,0 +1,167 @@
import {Constants} from "../constants";
import {setContentTheme} from "../ui/setContentTheme";
import {addScript, addScriptSync} from "../util/addScript";
import {hasClosestByClassName, hasClosestByMatchTag} from "../util/hasClosest";
import {merge} from "../util/merge";
import {abcRender} from "./abcRender";
import {anchorRender} from "./anchorRender";
import {chartRender} from "./chartRender";
import {codeRender} from "./codeRender";
import {flowchartRender} from "./flowchartRender";
import {graphvizRender} from "./graphvizRender";
import {highlightRender} from "./highlightRender";
import {lazyLoadImageRender} from "./lazyLoadImageRender";
import {mathRender} from "./mathRender";
import {mediaRender} from "./mediaRender";
import {mermaidRender} from "./mermaidRender";
import {markmapRender} from "../markdown/markmapRender";
import {mindmapRender} from "./mindmapRender";
import {plantumlRender} from "./plantumlRender";
import {setLute} from "./setLute";
import {speechRender} from "./speechRender";
const mergeOptions = (options?: IPreviewOptions) => {
const defaultOption: IPreviewOptions = {
anchor: 0,
cdn: Constants.CDN,
customEmoji: {},
emojiPath: `${Constants.CDN}/dist/images/emoji`,
hljs: Constants.HLJS_OPTIONS,
icon: "ant",
lang: "zh_CN",
markdown: Constants.MARKDOWN_OPTIONS,
math: Constants.MATH_OPTIONS,
mode: "light",
speech: {
enable: false,
},
render: {
media: {
enable: true,
}
},
theme: Constants.THEME_OPTIONS,
};
if (options.cdn) {
if (!options.theme?.path) {
defaultOption.theme.path = `${options.cdn}/dist/css/content-theme`
}
if (!options.emojiPath) {
defaultOption.emojiPath = `${options.cdn}/dist/images/emoji`;
}
}
return merge(defaultOption, options);
};
export const md2html = (mdText: string, options?: IPreviewOptions) => {
const mergedOptions = mergeOptions(options);
return addScript(`${mergedOptions.cdn}/dist/js/lute/lute.min.js`, "vditorLuteScript").then(() => {
const lute = setLute({
autoSpace: mergedOptions.markdown.autoSpace,
gfmAutoLink: mergedOptions.markdown.gfmAutoLink,
codeBlockPreview: mergedOptions.markdown.codeBlockPreview,
emojiSite: mergedOptions.emojiPath,
emojis: mergedOptions.customEmoji,
fixTermTypo: mergedOptions.markdown.fixTermTypo,
footnotes: mergedOptions.markdown.footnotes,
headingAnchor: mergedOptions.anchor !== 0,
inlineMathDigit: mergedOptions.math.inlineDigit,
lazyLoadImage: mergedOptions.lazyLoadImage,
linkBase: mergedOptions.markdown.linkBase,
linkPrefix: mergedOptions.markdown.linkPrefix,
listStyle: mergedOptions.markdown.listStyle,
mark: mergedOptions.markdown.mark,
mathBlockPreview: mergedOptions.markdown.mathBlockPreview,
paragraphBeginningSpace: mergedOptions.markdown.paragraphBeginningSpace,
sanitize: mergedOptions.markdown.sanitize,
toc: mergedOptions.markdown.toc,
});
if (options?.renderers) {
lute.SetJSRenderers({
renderers: {
Md2HTML: options.renderers,
},
});
}
lute.SetHeadingID(true);
return lute.Md2HTML(mdText);
});
};
export const previewRender = async (previewElement: HTMLDivElement, markdown: string, options?: IPreviewOptions) => {
const mergedOptions: IPreviewOptions = mergeOptions(options);
let html = await md2html(markdown, mergedOptions);
if (mergedOptions.transform) {
html = mergedOptions.transform(html);
}
previewElement.innerHTML = html;
previewElement.classList.add("vditor-reset");
if (!mergedOptions.i18n) {
if (!["en_US", "fr_FR", "pt_BR", "ja_JP", "ko_KR", "ru_RU", "sv_SE", "zh_CN", "zh_TW"].includes(mergedOptions.lang)) {
throw new Error(
"options.lang error, see https://ld246.com/article/1549638745630#options",
);
} else {
const i18nScriptPrefix = "vditorI18nScript";
const i18nScriptID = i18nScriptPrefix + mergedOptions.lang;
document.querySelectorAll(`head script[id^="${i18nScriptPrefix}"]`).forEach((el) => {
if (el.id !== i18nScriptID) {
document.head.removeChild(el);
}
});
await addScript(`${mergedOptions.cdn}/dist/js/i18n/${mergedOptions.lang}.js`, i18nScriptID);
}
} else {
window.VditorI18n = mergedOptions.i18n;
}
if (mergedOptions.icon) {
await addScript(`${mergedOptions.cdn}/dist/js/icons/${mergedOptions.icon}.js`, "vditorIconScript");
}
setContentTheme(mergedOptions.theme.current, mergedOptions.theme.path);
if (mergedOptions.anchor === 1) {
previewElement.classList.add("vditor-reset--anchor");
}
codeRender(previewElement, mergedOptions.hljs);
highlightRender(mergedOptions.hljs, previewElement, mergedOptions.cdn);
mathRender(previewElement, {
cdn: mergedOptions.cdn,
math: mergedOptions.math,
});
mermaidRender(previewElement, mergedOptions.cdn, mergedOptions.mode);
markmapRender(previewElement, mergedOptions.cdn, mergedOptions.mode);
flowchartRender(previewElement, mergedOptions.cdn);
graphvizRender(previewElement, mergedOptions.cdn);
chartRender(previewElement, mergedOptions.cdn, mergedOptions.mode);
mindmapRender(previewElement, mergedOptions.cdn, mergedOptions.mode);
plantumlRender(previewElement, mergedOptions.cdn);
abcRender(previewElement, mergedOptions.cdn);
if (mergedOptions.render.media.enable) {
mediaRender(previewElement);
}
if (mergedOptions.speech.enable) {
speechRender(previewElement);
}
if (mergedOptions.anchor !== 0) {
anchorRender(mergedOptions.anchor);
}
if (mergedOptions.after) {
mergedOptions.after();
}
if (mergedOptions.lazyLoadImage) {
lazyLoadImageRender(previewElement);
}
previewElement.addEventListener("click", (event: MouseEvent & { target: HTMLElement }) => {
const spanElement = hasClosestByMatchTag(event.target, "SPAN");
if (spanElement && hasClosestByClassName(spanElement, "vditor-toc")) {
const headingElement =
previewElement.querySelector("#" + spanElement.getAttribute("data-target-id")) as HTMLElement;
if (headingElement) {
window.scrollTo(window.scrollX, headingElement.offsetTop);
}
return;
}
});
};

View File

@@ -0,0 +1,24 @@
export const setLute = (options: ILuteOptions) => {
const lute: Lute = Lute.New();
lute.PutEmojis(options.emojis);
lute.SetEmojiSite(options.emojiSite);
lute.SetHeadingAnchor(options.headingAnchor);
lute.SetInlineMathAllowDigitAfterOpenMarker(options.inlineMathDigit);
lute.SetAutoSpace(options.autoSpace);
lute.SetToC(options.toc);
lute.SetFootnotes(options.footnotes);
lute.SetFixTermTypo(options.fixTermTypo);
lute.SetVditorCodeBlockPreview(options.codeBlockPreview);
lute.SetVditorMathBlockPreview(options.mathBlockPreview);
lute.SetSanitize(options.sanitize);
lute.SetChineseParagraphBeginningSpace(options.paragraphBeginningSpace);
lute.SetRenderListStyle(options.listStyle);
lute.SetLinkBase(options.linkBase);
lute.SetLinkPrefix(options.linkPrefix);
lute.SetMark(options.mark);
lute.SetGFMAutoLink(options.gfmAutoLink);
if (options.lazyLoadImage) {
lute.SetImageLazyLoading(options.lazyLoadImage);
}
return lute;
};

View File

@@ -0,0 +1,107 @@
import {setSelectionFocus} from "../util/selection";
declare global {
interface Window {
vditorSpeechRange: Range;
}
}
export const speechRender = (element: HTMLElement, lang: keyof II18n = "zh_CN") => {
if (typeof speechSynthesis === "undefined" || typeof SpeechSynthesisUtterance === "undefined") {
return;
}
const getVoice = () => {
const voices = speechSynthesis.getVoices();
let currentVoice: SpeechSynthesisVoice;
let defaultVoice;
voices.forEach((item) => {
if (item.lang === lang.replace("_", "-")) {
currentVoice = item;
}
if (item.default) {
defaultVoice = item;
}
});
if (!currentVoice) {
currentVoice = defaultVoice;
}
return currentVoice;
};
let playSVG = '<svg><use xlink:href="#vditor-icon-play"></use></svg>';
let pauseSVG = '<svg><use xlink:href="#vditor-icon-pause"></use></svg>';
if (!document.getElementById("vditorIconScript")) {
playSVG = '<svg viewBox="0 0 32 32"><path d="M3.436 0l25.128 16-25.128 16v-32z"></path></svg>';
pauseSVG = '<svg viewBox="0 0 32 32"><path d="M20.617 0h9.128v32h-9.128v-32zM2.255 32v-32h9.128v32h-9.128z"></path></svg>';
}
let speechDom: HTMLButtonElement = document.querySelector(".vditor-speech");
if (!speechDom) {
speechDom = document.createElement("button");
speechDom.className = "vditor-speech";
element.insertAdjacentElement("beforeend", speechDom);
if (speechSynthesis.onvoiceschanged !== undefined) {
speechSynthesis.onvoiceschanged = getVoice;
}
}
const voice = getVoice();
const utterThis = new SpeechSynthesisUtterance();
utterThis.voice = voice;
utterThis.onend = utterThis.onerror = () => {
speechDom.style.display = "none";
speechSynthesis.cancel();
speechDom.classList.remove("vditor-speech--current");
speechDom.innerHTML = playSVG;
};
element.addEventListener(window.ontouchstart !== undefined ? "touchend" : "click", (event) => {
const target = event.target as HTMLElement
if (target.classList.contains("vditor-speech") || target.parentElement.classList.contains("vditor-speech")) {
if (!speechDom.classList.contains("vditor-speech--current")) {
utterThis.text = speechDom.getAttribute("data-text");
speechSynthesis.speak(utterThis);
speechDom.classList.add("vditor-speech--current");
speechDom.innerHTML = pauseSVG;
} else {
if (speechSynthesis.speaking) {
if (speechSynthesis.paused) {
speechSynthesis.resume();
speechDom.innerHTML = pauseSVG;
} else {
speechSynthesis.pause();
speechDom.innerHTML = playSVG;
}
}
}
setSelectionFocus(window.vditorSpeechRange);
element.focus();
return;
}
speechDom.style.display = "none";
speechSynthesis.cancel();
speechDom.classList.remove("vditor-speech--current");
speechDom.innerHTML = playSVG;
if (getSelection().rangeCount === 0) {
return;
}
const range = getSelection().getRangeAt(0)
const text = range.toString().trim();
if (!text) {
return;
}
window.vditorSpeechRange = range.cloneRange();
const rect = range.getBoundingClientRect();
speechDom.innerHTML = playSVG;
speechDom.style.display = "block";
speechDom.style.top = (rect.top + rect.height + document.querySelector("html").scrollTop - 20) + "px";
if (window.ontouchstart !== undefined) {
speechDom.style.left = ((event as TouchEvent).changedTouches[(event as TouchEvent).changedTouches.length - 1].pageX + 2) + "px";
} else {
speechDom.style.left = ((event as MouseEvent).clientX + 2) + "px";
}
speechDom.setAttribute("data-text", text);
});
};