cherry_markdown.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691
  1. $(function () {
  2. window.editormdLocales = {
  3. 'zh-CN': {
  4. placeholder: '本编辑器支持 Markdown 编辑,左边编写,右边预览。',
  5. contentUnsaved: '编辑内容未保存,需要保存吗?',
  6. noDocNeedPublish: '没有需要发布的文档',
  7. loadDocFailed: '文档加载失败',
  8. fetchDocFailed: '获取当前文档信息失败',
  9. cannotAddToEmptyNode: '空节点不能添加内容',
  10. overrideModified: '文档已被其他人修改确定覆盖已存在的文档吗?',
  11. confirm: '确定',
  12. cancel: '取消',
  13. contentsNameEmpty: '目录名称不能为空',
  14. addDoc: '添加文档',
  15. edit: '编辑',
  16. delete: '删除',
  17. loadFailed: '加载失败请重试',
  18. tplNameEmpty: '模板名称不能为空',
  19. tplContentEmpty: '模板内容不能为空',
  20. saveSucc: '保存成功',
  21. serverExcept: '服务器异常',
  22. paramName: '参数名称',
  23. paramType: '参数类型',
  24. example: '示例值',
  25. remark: '备注',
  26. },
  27. 'en': {
  28. placeholder: 'This editor supports Markdown editing, writing on the left and previewing on the right.',
  29. contentUnsaved: 'The edited content is not saved, need to save it?',
  30. noDocNeedPublish: 'No Document need to be publish',
  31. loadDocFailed: 'Load Document failed',
  32. fetchDocFailed: 'Fetch Document info failed',
  33. cannotAddToEmptyNode: 'Cannot add content to empty node',
  34. overrideModified: 'The document has been modified by someone else, are you sure to overwrite the document?',
  35. confirm: 'Confirm',
  36. cancel: 'Cancel',
  37. contentsNameEmpty: 'Document Name cannot be empty',
  38. addDoc: 'Add Document',
  39. edit: 'Edit',
  40. delete: 'Delete',
  41. loadFailed: 'Failed to load, please try again',
  42. tplNameEmpty: 'Template name cannot be empty',
  43. tplContentEmpty: 'Template content cannot be empty',
  44. saveSucc: 'Save success',
  45. serverExcept: 'Server Exception',
  46. paramName: 'Parameter',
  47. paramType: 'Type',
  48. example: 'Example',
  49. remark: 'Remark',
  50. }
  51. };
  52. var CustomHookA = Cherry.createSyntaxHook('codeBlock', Cherry.constants.HOOKS_TYPE_LIST.PAR, {
  53. makeHtml(str) {
  54. console.warn('custom hook', 'hello');
  55. return str;
  56. },
  57. rule(str) {
  58. const regex = {
  59. begin: '',
  60. content: '',
  61. end: '',
  62. };
  63. regex.reg = new RegExp(regex.begin + regex.content + regex.end, 'g');
  64. return regex;
  65. },
  66. });
  67. /**
  68. * 自定义一个自定义菜单
  69. * 点第一次时,把选中的文字变成同时加粗和斜体
  70. * 保持光标选区不变,点第二次时,把加粗斜体的文字变成普通文本
  71. */
  72. var customMenuA = Cherry.createMenuHook('加粗斜体', {
  73. iconName: 'font',
  74. onClick: function (selection) {
  75. // 获取用户选中的文字,调用getSelection方法后,如果用户没有选中任何文字,会尝试获取光标所在位置的单词或句子
  76. let $selection = this.getSelection(selection) || '同时加粗斜体';
  77. // 如果是单选,并且选中内容的开始结束内没有加粗语法,则扩大选中范围
  78. if (!this.isSelections && !/^\s*(\*\*\*)[\s\S]+(\1)/.test($selection)) {
  79. this.getMoreSelection('***', '***', () => {
  80. const newSelection = this.editor.editor.getSelection();
  81. const isBoldItalic = /^\s*(\*\*\*)[\s\S]+(\1)/.test(newSelection);
  82. if (isBoldItalic) {
  83. $selection = newSelection;
  84. }
  85. return isBoldItalic;
  86. });
  87. }
  88. // 如果选中的文本中已经有加粗语法了,则去掉加粗语法
  89. if (/^\s*(\*\*\*)[\s\S]+(\1)/.test($selection)) {
  90. return $selection.replace(/(^)(\s*)(\*\*\*)([^\n]+)(\3)(\s*)($)/gm, '$1$4$7');
  91. }
  92. /**
  93. * 注册缩小选区的规则
  94. * 注册后,插入“***TEXT***”,选中状态会变成“***【TEXT】***”
  95. * 如果不注册,插入后效果为:“【***TEXT***】”
  96. */
  97. this.registerAfterClickCb(() => {
  98. this.setLessSelection('***', '***');
  99. });
  100. return $selection.replace(/(^)([^\n]+)($)/gm, '$1***$2***$3');
  101. }
  102. });
  103. /**
  104. * 定义一个空壳,用于自行规划cherry已有工具栏的层级结构
  105. */
  106. var customMenuB = Cherry.createMenuHook('发布', {
  107. iconName: 'publish',
  108. onClick: releaseDocument,
  109. });
  110. var customMenuC = Cherry.createMenuHook("返回", {
  111. iconName: 'back',
  112. onClick: backWard,
  113. })
  114. var customMenuD = Cherry.createMenuHook('保存', {
  115. id: "markdown-save",
  116. iconName: 'save',
  117. onClick: saveDocument,
  118. });
  119. var customMenuE = Cherry.createMenuHook('边栏', {
  120. iconName: 'sider',
  121. onClick: siderChange,
  122. });
  123. var customMenuF = Cherry.createMenuHook('历史', {
  124. iconName: 'history',
  125. onClick: showHistory,
  126. });
  127. var basicConfig = {
  128. id: 'manualEditorContainer',
  129. externals: {
  130. echarts: window.echarts,
  131. katex: window.katex,
  132. MathJax: window.MathJax,
  133. },
  134. isPreviewOnly: false,
  135. engine: {
  136. global: {
  137. urlProcessor(url, srcType) {
  138. //console.log(`url-processor`, url, srcType);
  139. return url;
  140. },
  141. },
  142. syntax: {
  143. codeBlock: {
  144. theme: 'twilight',
  145. },
  146. table: {
  147. enableChart: false,
  148. // chartEngine: Engine Class
  149. },
  150. fontEmphasis: {
  151. allowWhitespace: false, // 是否允许首尾空格
  152. },
  153. strikethrough: {
  154. needWhitespace: false, // 是否必须有前后空格
  155. },
  156. mathBlock: {
  157. engine: 'MathJax', // katex或MathJax
  158. src: 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js', // 如果使用MathJax plugins,则需要使用该url通过script标签引入
  159. },
  160. inlineMath: {
  161. engine: 'MathJax', // katex或MathJax
  162. },
  163. emoji: {
  164. useUnicode: false,
  165. customResourceURL: 'https://github.githubassets.com/images/icons/emoji/unicode/${code}.png?v8',
  166. upperCase: true,
  167. },
  168. // toc: {
  169. // tocStyle: 'nested'
  170. // }
  171. // 'header': {
  172. // strict: false
  173. // }
  174. },
  175. customSyntax: {
  176. // SyntaxHookClass
  177. CustomHook: {
  178. syntaxClass: CustomHookA,
  179. force: false,
  180. after: 'br',
  181. },
  182. },
  183. },
  184. toolbars: {
  185. toolbar: [
  186. 'customMenuCName',
  187. 'customMenuDName',
  188. 'customMenuBName',
  189. 'customMenuEName',
  190. 'undo',
  191. 'redo',
  192. 'bold',
  193. 'italic',
  194. {
  195. strikethrough: ['strikethrough', 'underline', 'sub', 'sup', 'ruby', 'customMenuAName'],
  196. },
  197. 'size',
  198. '|',
  199. 'color',
  200. 'header',
  201. '|',
  202. 'drawIo',
  203. '|',
  204. 'ol',
  205. 'ul',
  206. 'checklist',
  207. 'panel',
  208. 'detail',
  209. '|',
  210. 'formula',
  211. {
  212. insert: ['image', 'audio', 'video', 'link', 'hr', 'br', 'code', 'formula', 'toc', 'table', 'pdf', 'word', 'ruby'],
  213. },
  214. 'graph',
  215. 'togglePreview',
  216. 'settings',
  217. 'switchModel',
  218. 'export',
  219. 'customMenuFName',
  220. ],
  221. bubble: ['bold', 'italic', 'underline', 'strikethrough', 'sub', 'sup', 'quote', 'ruby', '|', 'size', 'color'], // array or false
  222. sidebar: ['mobilePreview', 'copy', 'codeTheme', 'theme'],
  223. customMenu: {
  224. customMenuAName: customMenuA,
  225. customMenuBName: customMenuB,
  226. customMenuCName: customMenuC,
  227. customMenuDName: customMenuD,
  228. customMenuEName: customMenuE,
  229. customMenuFName: customMenuF,
  230. },
  231. },
  232. drawioIframeUrl: '/static/cherry/drawio_demo.html',
  233. editor: {
  234. defaultModel: 'edit&preview',
  235. height: "100%",
  236. },
  237. previewer: {
  238. // 自定义markdown预览区域class
  239. // className: 'markdown'
  240. },
  241. keydown: [],
  242. //extensions: [],
  243. //callback: {
  244. //changeString2Pinyin: pinyin,
  245. //}
  246. };
  247. fetch('').then((response) => response.text()).then((value) => {
  248. //var markdownarea = document.getElementById("markdown_area").value
  249. var config = Object.assign({}, basicConfig);// { value: markdownarea });// { value: value });不显示获取的初始化值
  250. window.editor = new Cherry(config);
  251. window.editor.getCodeMirror().on('change', (e, detail)=>{
  252. resetEditorChanged(true);
  253. });
  254. openLastSelectedNode();
  255. uploadImage("manualEditorContainer", function ($state, $res) {
  256. console.log("注册上传图片")
  257. if ($state === "before") {
  258. return layer.load(1, {
  259. shade: [0.1, '#fff'] // 0.1 透明度的白色背景
  260. });
  261. } else if ($state === "success") {
  262. if ($res.errcode === 0) {
  263. var value = '![](' + $res.url + ')';
  264. window.editor.insertValue(value);
  265. }
  266. }
  267. });
  268. });
  269. /***
  270. * 加载指定的文档到编辑器中
  271. * @param $node
  272. */
  273. window.loadDocument = function ($node) {
  274. var index = layer.load(1, {
  275. shade: [0.1, '#fff'] // 0.1 透明度的白色背景
  276. });
  277. $.get(window.editURL + $node.node.id).done(function (res) {
  278. layer.close(index);
  279. if (res.errcode === 0) {
  280. window.isLoad = true;
  281. try {
  282. window.editor.setTheme(res.data.markdown_theme);
  283. window.editor.setMarkdown(res.data.markdown);
  284. } catch (e) {
  285. console.log(e);
  286. }
  287. var node = { "id": res.data.doc_id, 'parent': res.data.parent_id === 0 ? '#' : res.data.parent_id, "text": res.data.doc_name, "identify": res.data.identify, "version": res.data.version };
  288. pushDocumentCategory(node);
  289. window.selectNode = node;
  290. pushVueLists(res.data.attach);
  291. setLastSelectNode($node);
  292. } else {
  293. layer.msg(editormdLocales[lang].loadDocFailed);
  294. }
  295. }).fail(function () {
  296. layer.close(index);
  297. layer.msg(editormdLocales[lang].loadDocFailed);
  298. });
  299. };
  300. /**
  301. * 保存文档到服务器
  302. * @param $is_cover 是否强制覆盖
  303. */
  304. function saveDocument($is_cover, callback) {
  305. var index = null;
  306. var node = window.selectNode;
  307. var content = window.editor.getMarkdown();
  308. var html = window.editor.getHtml(true);
  309. var markdownTheme = window.editor.getTheme();
  310. var version = "";
  311. if (!node) {
  312. layer.msg(editormdLocales[lang].fetchDocFailed);
  313. return;
  314. }
  315. if (node.a_attr && node.a_attr.disabled) {
  316. layer.msg(editormdLocales[lang].cannotAddToEmptyNode);
  317. return;
  318. }
  319. var doc_id = parseInt(node.id);
  320. for (var i in window.documentCategory) {
  321. var item = window.documentCategory[i];
  322. if (item.id === doc_id) {
  323. version = item.version;
  324. break;
  325. }
  326. }
  327. $.ajax({
  328. beforeSend: function () {
  329. index = layer.load(1, { shade: [0.1, '#fff'] });
  330. window.saveing = true;
  331. },
  332. url: window.editURL,
  333. data: { "identify": window.book.identify, "doc_id": doc_id, "markdown": content, "html": html, "markdown_theme": markdownTheme, "cover": $is_cover ? "yes" : "no", "version": version },
  334. type: "post",
  335. timeout: 30000,
  336. dataType: "json",
  337. success: function (res) {
  338. if (res.errcode === 0) {
  339. resetEditorChanged(false);
  340. for (var i in window.documentCategory) {
  341. var item = window.documentCategory[i];
  342. if (item.id === doc_id) {
  343. window.documentCategory[i].version = res.data.version;
  344. break;
  345. }
  346. }
  347. $.each(window.documentCategory, function (i, item) {
  348. var $item = window.documentCategory[i];
  349. if (item.id === doc_id) {
  350. window.documentCategory[i].version = res.data.version;
  351. }
  352. });
  353. if (typeof callback === "function") {
  354. callback();
  355. }
  356. } else if (res.errcode === 6005) {
  357. var confirmIndex = layer.confirm(editormdLocales[lang].overrideModified, {
  358. btn: [editormdLocales[lang].confirm, editormdLocales[lang].cancel] // 按钮
  359. }, function () {
  360. layer.close(confirmIndex);
  361. saveDocument(true, callback);
  362. });
  363. } else {
  364. layer.msg(res.message);
  365. }
  366. },
  367. error: function (XMLHttpRequest, textStatus, errorThrown) {
  368. layer.msg(window.editormdLocales[window.lang].serverExcept + errorThrown);
  369. },
  370. complete: function () {
  371. layer.close(index);
  372. window.saveing = false;
  373. }
  374. });
  375. }
  376. /**
  377. * 设置编辑器变更状态
  378. * @param $is_change
  379. */
  380. function resetEditorChanged($is_change) {
  381. if ($is_change && !window.isLoad) {
  382. $("#markdown-save").removeClass('disabled').addClass('change');
  383. } else {
  384. $("#markdown-save").removeClass('change').addClass('disabled');
  385. }
  386. window.isLoad = false;
  387. }
  388. /**
  389. * 返回上一个页面
  390. */
  391. function backWard() {
  392. if (document.referrer == "") { // 没有上一级
  393. var homepage = window.location.origin;
  394. window.location.href = homepage; // 返回首页
  395. return;
  396. }
  397. window.location.href = document.referrer;
  398. }
  399. /**
  400. * 发布文档
  401. */
  402. function releaseDocument() {
  403. if (Object.prototype.toString.call(window.documentCategory) === '[object Array]' && window.documentCategory.length > 0) {
  404. if ($("#markdown-save").hasClass('change')) {
  405. var confirm_result = confirm(editormdLocales[lang].contentUnsaved);
  406. if (confirm_result) {
  407. saveDocument(false, releaseBook);
  408. return;
  409. }
  410. }
  411. releaseBook();
  412. return
  413. }
  414. layer.msg(editormdLocales[lang].noDocNeedPublish)
  415. }
  416. /**
  417. * 显示/隐藏边栏
  418. */
  419. function siderChange() {
  420. $("#manualCategory").toggle(0, "swing", function () {
  421. var $then = $("#manualEditorContainer");
  422. var left = parseInt($then.css("left"));
  423. if (left > 0) {
  424. window.editorContainerLeft = left;
  425. $then.css("left", "0");
  426. } else {
  427. $then.css("left", window.editorContainerLeft + "px");
  428. }
  429. });
  430. }
  431. /**
  432. * 显示文档历史
  433. */
  434. function showHistory() {
  435. window.documentHistory();
  436. }
  437. /**
  438. * 添加文档
  439. */
  440. $("#addDocumentForm").ajaxForm({
  441. beforeSubmit: function () {
  442. var doc_name = $.trim($("#documentName").val());
  443. if (doc_name === "") {
  444. return showError(editormdLocales[lang].contentsNameEmpty, "#add-error-message")
  445. }
  446. $("#btnSaveDocument").button("loading");
  447. return true;
  448. },
  449. success: function (res) {
  450. if (res.errcode === 0) {
  451. var data = {
  452. "id": res.data.doc_id,
  453. 'parent': res.data.parent_id === 0 ? '#' : res.data.parent_id,
  454. "text": res.data.doc_name,
  455. "identify": res.data.identify,
  456. "version": res.data.version,
  457. state: { opened: res.data.is_open == 1 },
  458. a_attr: { is_open: res.data.is_open == 1 }
  459. };
  460. var node = window.treeCatalog.get_node(data.id);
  461. if (node) {
  462. window.treeCatalog.rename_node({ "id": data.id }, data.text);
  463. $("#sidebar").jstree(true).get_node(data.id).a_attr.is_open = data.state.opened;
  464. } else {
  465. window.treeCatalog.create_node(data.parent, data);
  466. window.treeCatalog.deselect_all();
  467. window.treeCatalog.select_node(data);
  468. }
  469. pushDocumentCategory(data);
  470. $("#markdown-save").removeClass('change').addClass('disabled');
  471. $("#addDocumentModal").modal('hide');
  472. } else {
  473. showError(res.message, "#add-error-message");
  474. }
  475. $("#btnSaveDocument").button("reset");
  476. }
  477. });
  478. /**
  479. * 文档目录树
  480. */
  481. $("#sidebar").jstree({
  482. 'plugins': ["wholerow", "types", 'dnd', 'contextmenu'],
  483. "types": {
  484. "default": {
  485. "icon": false // 删除默认图标
  486. }
  487. },
  488. 'core': {
  489. 'check_callback': true,
  490. "multiple": false,
  491. 'animation': 0,
  492. "data": window.documentCategory
  493. },
  494. "contextmenu": {
  495. show_at_node: false,
  496. select_node: false,
  497. "items": {
  498. "添加文档": {
  499. "separator_before": false,
  500. "separator_after": true,
  501. "_disabled": false,
  502. "label": window.editormdLocales[window.lang].addDoc,//"添加文档",
  503. "icon": "fa fa-plus",
  504. "action": function (data) {
  505. var inst = $.jstree.reference(data.reference),
  506. node = inst.get_node(data.reference);
  507. openCreateCatalogDialog(node);
  508. }
  509. },
  510. "编辑": {
  511. "separator_before": false,
  512. "separator_after": true,
  513. "_disabled": false,
  514. "label": window.editormdLocales[window.lang].edit,
  515. "icon": "fa fa-edit",
  516. "action": function (data) {
  517. var inst = $.jstree.reference(data.reference);
  518. var node = inst.get_node(data.reference);
  519. openEditCatalogDialog(node);
  520. }
  521. },
  522. "删除": {
  523. "separator_before": false,
  524. "separator_after": true,
  525. "_disabled": false,
  526. "label": window.editormdLocales[window.lang].delete,
  527. "icon": "fa fa-trash-o",
  528. "action": function (data) {
  529. var inst = $.jstree.reference(data.reference);
  530. var node = inst.get_node(data.reference);
  531. openDeleteDocumentDialog(node);
  532. }
  533. }
  534. }
  535. }
  536. }).on("ready.jstree", function () {
  537. window.treeCatalog = $("#sidebar").jstree(true);
  538. //如果没有选中节点则选中默认节点
  539. // openLastSelectedNode();
  540. }).on('select_node.jstree', function (node, selected) {
  541. if ($("#markdown-save").hasClass('change')) {
  542. if (confirm(window.editormdLocales[window.lang].contentUnsaved)) {
  543. saveDocument(false, function () {
  544. loadDocument(selected);
  545. });
  546. return true;
  547. }
  548. }
  549. //如果是空目录则直接出发展开下一级功能
  550. if (selected.node.a_attr && selected.node.a_attr.disabled) {
  551. selected.instance.toggle_node(selected.node);
  552. return false
  553. }
  554. loadDocument(selected);
  555. }).on("move_node.jstree", jstree_save).on("delete_node.jstree", function ($node, $parent) {
  556. openLastSelectedNode();
  557. });
  558. /**
  559. * 打开文档模板
  560. */
  561. $("#documentTemplateModal").on("click", ".section>a[data-type]", function () {
  562. var $this = $(this).attr("data-type");
  563. if ($this === "customs") {
  564. $("#displayCustomsTemplateModal").modal("show");
  565. return;
  566. }
  567. var body = $("#template-" + $this).html();
  568. if (body) {
  569. window.isLoad = true;
  570. window.editor.clear();
  571. window.editor.insertValue(body);
  572. window.editor.setCursor({ line: 0, ch: 0 });
  573. resetEditorChanged(true);
  574. }
  575. $("#documentTemplateModal").modal('hide');
  576. });
  577. });
  578. function uploadImage($id, $callback) {
  579. locales = {
  580. 'zh-CN': {
  581. unsupportType: '不支持的图片格式',
  582. uploadFailed: '图片上传失败'
  583. },
  584. 'en': {
  585. unsupportType: 'Unsupport image type',
  586. uploadFailed: 'Upload image failed'
  587. }
  588. }
  589. /** 粘贴上传图片 **/
  590. document.getElementById($id).addEventListener('paste', function (e) {
  591. if (e.clipboardData && e.clipboardData.items) {
  592. var clipboard = e.clipboardData;
  593. for (var i = 0, len = clipboard.items.length; i < len; i++) {
  594. if (clipboard.items[i].kind === 'file' || clipboard.items[i].type.indexOf('image') > -1) {
  595. var imageFile = clipboard.items[i].getAsFile();
  596. var fileName = String((new Date()).valueOf());
  597. switch (imageFile.type) {
  598. case "image/png" :
  599. fileName += ".png";
  600. break;
  601. case "image/jpg" :
  602. fileName += ".jpg";
  603. break;
  604. case "image/jpeg" :
  605. fileName += ".jpeg";
  606. break;
  607. case "image/gif" :
  608. fileName += ".gif";
  609. break;
  610. default :
  611. layer.msg(locales[lang].unsupportType);
  612. return;
  613. }
  614. var form = new FormData();
  615. form.append('editormd-image-file', imageFile, fileName);
  616. var layerIndex = 0;
  617. $.ajax({
  618. url: window.imageUploadURL,
  619. type: "POST",
  620. dataType: "json",
  621. data: form,
  622. processData: false,
  623. contentType: false,
  624. beforeSend: function () {
  625. layerIndex = $callback('before');
  626. },
  627. error: function () {
  628. layer.close(layerIndex);
  629. $callback('error');
  630. layer.msg(locales[lang].uploadFailed);
  631. },
  632. success: function (data) {
  633. layer.close(layerIndex);
  634. $callback('success', data);
  635. if (data.errcode !== 0) {
  636. layer.msg(data.message);
  637. }
  638. }
  639. });
  640. e.preventDefault();
  641. }
  642. }
  643. }
  644. });
  645. }