index.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454
  1. import VditorMethod from "./method";
  2. import {Constants, VDITOR_VERSION} from "./ts/constants";
  3. import {DevTools} from "./ts/devtools";
  4. import {Hint} from "./ts/hint/index";
  5. import {i18n} from "./ts/i18n";
  6. import {IR} from "./ts/ir";
  7. import {input as irInput} from "./ts/ir/input";
  8. import {processAfterRender} from "./ts/ir/process";
  9. import {getHTML} from "./ts/markdown/getHTML";
  10. import {getMarkdown} from "./ts/markdown/getMarkdown";
  11. import {setLute} from "./ts/markdown/setLute";
  12. import {Outline} from "./ts/outline";
  13. import {Preview} from "./ts/preview/index";
  14. import {Resize} from "./ts/resize/index";
  15. import {Editor} from "./ts/sv/index";
  16. import {inputEvent} from "./ts/sv/inputEvent";
  17. import {processAfterRender as processSVAfterRender} from "./ts/sv/process";
  18. import {Tip} from "./ts/tip";
  19. import {Toolbar} from "./ts/toolbar/index";
  20. import {disableToolbar, hidePanel} from "./ts/toolbar/setToolbar";
  21. import {enableToolbar} from "./ts/toolbar/setToolbar";
  22. import {initUI} from "./ts/ui/initUI";
  23. import {setCodeTheme} from "./ts/ui/setCodeTheme";
  24. import {setContentTheme} from "./ts/ui/setContentTheme";
  25. import {setPreviewMode} from "./ts/ui/setPreviewMode";
  26. import {setTheme} from "./ts/ui/setTheme";
  27. import {Undo} from "./ts/undo";
  28. import {Upload} from "./ts/upload/index";
  29. import {addScript, addScriptSync} from "./ts/util/addScript";
  30. import {getSelectText} from "./ts/util/getSelectText";
  31. import {Options} from "./ts/util/Options";
  32. import {processCodeRender} from "./ts/util/processCode";
  33. import {getCursorPosition, getEditorRange} from "./ts/util/selection";
  34. import {WYSIWYG} from "./ts/wysiwyg";
  35. import {afterRenderEvent} from "./ts/wysiwyg/afterRenderEvent";
  36. import {input} from "./ts/wysiwyg/input";
  37. import {renderDomByMd} from "./ts/wysiwyg/renderDomByMd";
  38. class Vditor extends VditorMethod {
  39. public readonly version: string;
  40. public vditor: IVditor;
  41. /**
  42. * @param id 要挂载 Vditor 的元素或者元素 ID。
  43. * @param options Vditor 参数
  44. */
  45. constructor(id: string | HTMLElement, options?: IOptions) {
  46. super();
  47. this.version = VDITOR_VERSION;
  48. if (typeof id === "string") {
  49. if (!options) {
  50. options = {
  51. cache: {
  52. id: `vditor${id}`,
  53. },
  54. };
  55. } else if (!options.cache) {
  56. options.cache = {id: `vditor${id}`};
  57. } else if (!options.cache.id) {
  58. options.cache.id = `vditor${id}`;
  59. }
  60. id = document.getElementById(id);
  61. }
  62. const getOptions = new Options(options);
  63. const mergedOptions = getOptions.merge();
  64. if (!["en_US", "ja_JP", "ko_KR", "zh_CN"].includes(mergedOptions.lang)) {
  65. throw new Error("options.lang error, see https://ld246.com/article/1549638745630#options");
  66. }
  67. this.vditor = {
  68. currentMode: mergedOptions.mode,
  69. element: id,
  70. hint: new Hint(mergedOptions.hint.extend),
  71. lute: undefined,
  72. options: mergedOptions,
  73. originalInnerHTML: id.innerHTML,
  74. outline: new Outline(i18n[mergedOptions.lang].outline),
  75. tip: new Tip(),
  76. };
  77. this.vditor.sv = new Editor(this.vditor);
  78. this.vditor.undo = new Undo();
  79. this.vditor.wysiwyg = new WYSIWYG(this.vditor);
  80. this.vditor.ir = new IR(this.vditor);
  81. this.vditor.toolbar = new Toolbar(this.vditor);
  82. if (mergedOptions.resize.enable) {
  83. this.vditor.resize = new Resize(this.vditor);
  84. }
  85. if (this.vditor.toolbar.elements.devtools) {
  86. this.vditor.devtools = new DevTools();
  87. }
  88. if (mergedOptions.upload.url || mergedOptions.upload.handler) {
  89. this.vditor.upload = new Upload();
  90. }
  91. addScript(options._lutePath || `${mergedOptions.cdn}/dist/js/lute/lute.min.js`, "vditorLuteScript")
  92. .then(() => {
  93. this.vditor.lute = setLute({
  94. autoSpace: this.vditor.options.preview.markdown.autoSpace,
  95. chinesePunct: this.vditor.options.preview.markdown.chinesePunct,
  96. codeBlockPreview: this.vditor.options.preview.markdown.codeBlockPreview,
  97. emojiSite: this.vditor.options.hint.emojiPath,
  98. emojis: this.vditor.options.hint.emoji,
  99. fixTermTypo: this.vditor.options.preview.markdown.fixTermTypo,
  100. footnotes: this.vditor.options.preview.markdown.footnotes,
  101. headingAnchor: false,
  102. inlineMathDigit: this.vditor.options.preview.math.inlineDigit,
  103. linkBase: this.vditor.options.preview.markdown.linkBase,
  104. linkPrefix: this.vditor.options.preview.markdown.linkPrefix,
  105. listStyle: this.vditor.options.preview.markdown.listStyle,
  106. mark: this.vditor.options.preview.markdown.mark,
  107. mathBlockPreview: this.vditor.options.preview.markdown.mathBlockPreview,
  108. paragraphBeginningSpace: this.vditor.options.preview.markdown.paragraphBeginningSpace,
  109. sanitize: this.vditor.options.preview.markdown.sanitize,
  110. toc: this.vditor.options.preview.markdown.toc,
  111. });
  112. this.vditor.preview = new Preview(this.vditor);
  113. initUI(this.vditor);
  114. if (mergedOptions.after) {
  115. mergedOptions.after();
  116. }
  117. if (mergedOptions.icon) {
  118. // 防止初始化 2 个编辑器时加载 2 次
  119. addScriptSync(`${mergedOptions.cdn}/dist/js/icons/${mergedOptions.icon}.js`, "vditorIconScript");
  120. }
  121. });
  122. }
  123. /** 设置主题 */
  124. public setTheme(theme: "dark" | "classic", contentTheme?: string, codeTheme?: string, contentThemePath?: string) {
  125. this.vditor.options.theme = theme;
  126. setTheme(this.vditor);
  127. if (contentTheme) {
  128. this.vditor.options.preview.theme.current = contentTheme;
  129. setContentTheme(contentTheme, contentThemePath || this.vditor.options.preview.theme.path);
  130. }
  131. if (codeTheme) {
  132. this.vditor.options.preview.hljs.style = codeTheme;
  133. setCodeTheme(codeTheme, this.vditor.options.cdn);
  134. }
  135. }
  136. /** 获取 Markdown 内容 */
  137. public getValue() {
  138. return getMarkdown(this.vditor);
  139. }
  140. /** 获取编辑器当前编辑模式 */
  141. public getCurrentMode() {
  142. return this.vditor.currentMode;
  143. }
  144. /** 聚焦到编辑器 */
  145. public focus() {
  146. if (this.vditor.currentMode === "sv") {
  147. this.vditor.sv.element.focus();
  148. } else if (this.vditor.currentMode === "wysiwyg") {
  149. this.vditor.wysiwyg.element.focus();
  150. } else if (this.vditor.currentMode === "ir") {
  151. this.vditor.ir.element.focus();
  152. }
  153. }
  154. /** 让编辑器失焦 */
  155. public blur() {
  156. if (this.vditor.currentMode === "sv") {
  157. this.vditor.sv.element.blur();
  158. } else if (this.vditor.currentMode === "wysiwyg") {
  159. this.vditor.wysiwyg.element.blur();
  160. } else if (this.vditor.currentMode === "ir") {
  161. this.vditor.ir.element.blur();
  162. }
  163. }
  164. /** 禁用编辑器 */
  165. public disabled() {
  166. hidePanel(this.vditor, ["subToolbar", "hint", "popover"]);
  167. disableToolbar(this.vditor.toolbar.elements, Constants.EDIT_TOOLBARS.concat(["undo", "redo", "fullscreen",
  168. "edit-mode"]));
  169. this.vditor[this.vditor.currentMode].element.setAttribute("contenteditable", "false");
  170. }
  171. /** 解除编辑器禁用 */
  172. public enable() {
  173. enableToolbar(this.vditor.toolbar.elements, Constants.EDIT_TOOLBARS.concat(["undo", "redo", "fullscreen",
  174. "edit-mode"]));
  175. this.vditor.undo.resetIcon(this.vditor);
  176. this.vditor[this.vditor.currentMode].element.setAttribute("contenteditable", "true");
  177. }
  178. /** 返回选中的字符串 */
  179. public getSelection() {
  180. if (this.vditor.currentMode === "wysiwyg") {
  181. return getSelectText(this.vditor.wysiwyg.element);
  182. } else if (this.vditor.currentMode === "sv") {
  183. return getSelectText(this.vditor.sv.element);
  184. } else if (this.vditor.currentMode === "ir") {
  185. return getSelectText(this.vditor.ir.element);
  186. }
  187. }
  188. /** 设置预览区域内容 */
  189. public renderPreview(value?: string) {
  190. this.vditor.preview.render(this.vditor, value);
  191. }
  192. /** 获取焦点位置 */
  193. public getCursorPosition() {
  194. return getCursorPosition(this.vditor[this.vditor.currentMode].element);
  195. }
  196. /** 上传是否还在进行中 */
  197. public isUploading() {
  198. return this.vditor.upload.isUploading;
  199. }
  200. /** 清除缓存 */
  201. public clearCache() {
  202. localStorage.removeItem(this.vditor.options.cache.id);
  203. }
  204. /** 禁用缓存 */
  205. public disabledCache() {
  206. this.vditor.options.cache.enable = false;
  207. }
  208. /** 启用缓存 */
  209. public enableCache() {
  210. if (!this.vditor.options.cache.id) {
  211. throw new Error("need options.cache.id, see https://ld246.com/article/1549638745630#options");
  212. return;
  213. }
  214. this.vditor.options.cache.enable = true;
  215. }
  216. /** HTML 转 md */
  217. public html2md(value: string) {
  218. return this.vditor.lute.HTML2Md(value);
  219. }
  220. /** 获取 HTML */
  221. public getHTML() {
  222. return getHTML(this.vditor);
  223. }
  224. /** 消息提示。time 为 0 将一直显示 */
  225. public tip(text: string, time?: number) {
  226. this.vditor.tip.show(text, time);
  227. }
  228. /** 设置预览模式 */
  229. public setPreviewMode(mode: "both" | "editor") {
  230. setPreviewMode(mode, this.vditor);
  231. }
  232. /** 删除选中内容 */
  233. public deleteValue() {
  234. if (window.getSelection().isCollapsed) {
  235. return;
  236. }
  237. document.execCommand("delete", false);
  238. }
  239. /** 更新选中内容 */
  240. public updateValue(value: string) {
  241. document.execCommand("insertHTML", false, value);
  242. }
  243. /** 在焦点处插入内容,并默认进行 Markdown 渲染 */
  244. public insertValue(value: string, render = true) {
  245. const range = getEditorRange(this.vditor[this.vditor.currentMode].element);
  246. range.collapse(true);
  247. // https://github.com/Vanessa219/vditor/issues/716 需使用 insertText,否则需要重写方法,不能使用 execCommand
  248. if (this.vditor.currentMode === "sv") {
  249. this.vditor.sv.preventInput = true;
  250. document.execCommand("insertText", false, value);
  251. if (render) {
  252. inputEvent(this.vditor);
  253. }
  254. } else if (this.vditor.currentMode === "wysiwyg") {
  255. this.vditor.wysiwyg.preventInput = true;
  256. document.execCommand("insertText", false, value);
  257. if (render) {
  258. input(this.vditor, getSelection().getRangeAt(0));
  259. }
  260. } else if (this.vditor.currentMode === "ir") {
  261. this.vditor.ir.preventInput = true;
  262. document.execCommand("insertText", false, value);
  263. if (render) {
  264. irInput(this.vditor, getSelection().getRangeAt(0), true);
  265. }
  266. }
  267. }
  268. /** 设置编辑器内容 */
  269. public setValue(markdown: string, clearStack = false) {
  270. if (this.vditor.currentMode === "sv") {
  271. this.vditor.sv.element.innerHTML = this.vditor.lute.SpinVditorSVDOM(markdown);
  272. processSVAfterRender(this.vditor, {
  273. enableAddUndoStack: clearStack,
  274. enableHint: false,
  275. enableInput: false,
  276. });
  277. } else if (this.vditor.currentMode === "wysiwyg") {
  278. renderDomByMd(this.vditor, markdown, {
  279. enableAddUndoStack: clearStack,
  280. enableHint: false,
  281. enableInput: false,
  282. });
  283. } else {
  284. this.vditor.ir.element.innerHTML = this.vditor.lute.Md2VditorIRDOM(markdown);
  285. this.vditor.ir.element.querySelectorAll(".vditor-ir__preview[data-render='2']").forEach(
  286. (item: HTMLElement) => {
  287. processCodeRender(item, this.vditor);
  288. });
  289. processAfterRender(this.vditor, {
  290. enableAddUndoStack: clearStack,
  291. enableHint: false,
  292. enableInput: false,
  293. });
  294. }
  295. this.vditor.outline.render(this.vditor);
  296. if (!markdown) {
  297. hidePanel(this.vditor, ["emoji", "headings", "submenu", "hint"]);
  298. if (this.vditor.wysiwyg.popover) {
  299. this.vditor.wysiwyg.popover.style.display = "none";
  300. }
  301. this.clearCache();
  302. }
  303. if (clearStack) {
  304. this.clearStack();
  305. }
  306. }
  307. /** 清空 undo & redo 栈 */
  308. public clearStack() {
  309. this.vditor.undo.clearStack(this.vditor);
  310. this.vditor.undo.addToUndoStack(this.vditor);
  311. }
  312. /** 销毁编辑器 */
  313. public destroy() {
  314. this.vditor.element.innerHTML = this.vditor.originalInnerHTML;
  315. this.vditor.element.classList.remove("vditor");
  316. this.vditor.element.removeAttribute("style");
  317. document.getElementById("vditorIconScript").remove();
  318. this.clearCache();
  319. }
  320. /** 获取评论 ID */
  321. public getCommentIds() {
  322. if (this.vditor.currentMode !== "wysiwyg") {
  323. return [];
  324. }
  325. return this.vditor.wysiwyg.getComments(this.vditor);
  326. }
  327. /** 高亮评论 */
  328. public hlCommentIds(ids: string[]) {
  329. if (this.vditor.currentMode !== "wysiwyg") {
  330. return;
  331. }
  332. const hlItem = (item: Element) => {
  333. item.classList.remove("vditor-comment--hover");
  334. ids.forEach((id) => {
  335. if (item.getAttribute("data-cmtids").indexOf(id) > -1) {
  336. item.classList.add("vditor-comment--hover");
  337. }
  338. });
  339. };
  340. this.vditor.wysiwyg.element.querySelectorAll(".vditor-comment").forEach((item) => {
  341. hlItem(item);
  342. });
  343. if (this.vditor.preview.element.style.display !== "none") {
  344. this.vditor.preview.element.querySelectorAll(".vditor-comment").forEach((item) => {
  345. hlItem(item);
  346. });
  347. }
  348. }
  349. /** 取消评论高亮 */
  350. public unHlCommentIds(ids: string[]) {
  351. if (this.vditor.currentMode !== "wysiwyg") {
  352. return;
  353. }
  354. const unHlItem = (item: Element) => {
  355. ids.forEach((id) => {
  356. if (item.getAttribute("data-cmtids").indexOf(id) > -1) {
  357. item.classList.remove("vditor-comment--hover");
  358. }
  359. });
  360. };
  361. this.vditor.wysiwyg.element.querySelectorAll(".vditor-comment").forEach((item) => {
  362. unHlItem(item);
  363. });
  364. if (this.vditor.preview.element.style.display !== "none") {
  365. this.vditor.preview.element.querySelectorAll(".vditor-comment").forEach((item) => {
  366. unHlItem(item);
  367. });
  368. }
  369. }
  370. /** 删除评论 */
  371. public removeCommentIds(removeIds: string[]) {
  372. if (this.vditor.currentMode !== "wysiwyg") {
  373. return;
  374. }
  375. const removeItem = (item: Element, removeId: string) => {
  376. const ids = item.getAttribute("data-cmtids").split(" ");
  377. ids.find((id, index) => {
  378. if (id === removeId) {
  379. ids.splice(index, 1);
  380. return true;
  381. }
  382. });
  383. if (ids.length === 0) {
  384. item.outerHTML = item.innerHTML;
  385. getEditorRange(this.vditor.element).collapse(true);
  386. } else {
  387. item.setAttribute("data-cmtids", ids.join(" "));
  388. }
  389. };
  390. removeIds.forEach((removeId) => {
  391. this.vditor.wysiwyg.element.querySelectorAll(".vditor-comment").forEach((item) => {
  392. removeItem(item, removeId);
  393. });
  394. if (this.vditor.preview.element.style.display !== "none") {
  395. this.vditor.preview.element.querySelectorAll(".vditor-comment").forEach((item) => {
  396. removeItem(item, removeId);
  397. });
  398. }
  399. });
  400. afterRenderEvent(this.vditor, {
  401. enableAddUndoStack: true,
  402. enableHint: false,
  403. enableInput: false,
  404. });
  405. }
  406. }
  407. export default Vditor;