cherry_markdown.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641
  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. fileUpload: myFileUpload,
  136. engine: {
  137. global: {
  138. urlProcessor(url, srcType) {
  139. //console.log(`url-processor`, url, srcType);
  140. return url;
  141. },
  142. },
  143. syntax: {
  144. codeBlock: {
  145. theme: 'twilight',
  146. },
  147. table: {
  148. enableChart: false,
  149. // chartEngine: Engine Class
  150. },
  151. fontEmphasis: {
  152. allowWhitespace: false, // 是否允许首尾空格
  153. },
  154. strikethrough: {
  155. needWhitespace: false, // 是否必须有前后空格
  156. },
  157. mathBlock: {
  158. engine: 'MathJax', // katex或MathJax
  159. src: 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js', // 如果使用MathJax plugins,则需要使用该url通过script标签引入
  160. },
  161. inlineMath: {
  162. engine: 'MathJax', // katex或MathJax
  163. },
  164. emoji: {
  165. useUnicode: false,
  166. customResourceURL: 'https://github.githubassets.com/images/icons/emoji/unicode/${code}.png?v8',
  167. upperCase: true,
  168. },
  169. // toc: {
  170. // tocStyle: 'nested'
  171. // }
  172. // 'header': {
  173. // strict: false
  174. // }
  175. },
  176. customSyntax: {
  177. // SyntaxHookClass
  178. CustomHook: {
  179. syntaxClass: CustomHookA,
  180. force: false,
  181. after: 'br',
  182. },
  183. },
  184. },
  185. toolbars: {
  186. toolbar: [
  187. 'customMenuCName',
  188. 'customMenuDName',
  189. 'customMenuBName',
  190. 'customMenuEName',
  191. 'undo',
  192. 'redo',
  193. 'bold',
  194. 'italic',
  195. {
  196. strikethrough: ['strikethrough', 'underline', 'sub', 'sup', 'ruby', 'customMenuAName'],
  197. },
  198. 'size',
  199. '|',
  200. 'color',
  201. 'header',
  202. '|',
  203. 'drawIo',
  204. '|',
  205. 'ol',
  206. 'ul',
  207. 'checklist',
  208. 'panel',
  209. 'detail',
  210. '|',
  211. 'formula',
  212. {
  213. insert: ['image', 'audio', 'video', 'link', 'hr', 'br', 'code', 'formula', 'toc', 'table', 'pdf', 'word', 'ruby'],
  214. },
  215. 'graph',
  216. 'togglePreview',
  217. 'settings',
  218. 'switchModel',
  219. 'export',
  220. 'customMenuFName',
  221. ],
  222. bubble: ['bold', 'italic', 'underline', 'strikethrough', 'sub', 'sup', 'quote', 'ruby', '|', 'size', 'color'], // array or false
  223. sidebar: ['mobilePreview', 'copy', 'codeTheme', 'theme'],
  224. customMenu: {
  225. customMenuAName: customMenuA,
  226. customMenuBName: customMenuB,
  227. customMenuCName: customMenuC,
  228. customMenuDName: customMenuD,
  229. customMenuEName: customMenuE,
  230. customMenuFName: customMenuF,
  231. },
  232. },
  233. drawioIframeUrl: '/static/cherry/drawio_demo.html',
  234. editor: {
  235. defaultModel: 'edit&preview',
  236. height: "100%",
  237. },
  238. previewer: {
  239. // 自定义markdown预览区域class
  240. // className: 'markdown'
  241. },
  242. keydown: [],
  243. //extensions: [],
  244. //callback: {
  245. //changeString2Pinyin: pinyin,
  246. //}
  247. };
  248. fetch('').then((response) => response.text()).then((value) => {
  249. //var markdownarea = document.getElementById("markdown_area").value
  250. var config = Object.assign({}, basicConfig);// { value: markdownarea });// { value: value });不显示获取的初始化值
  251. window.editor = new Cherry(config);
  252. window.editor.getCodeMirror().on('change', (e, detail)=>{
  253. resetEditorChanged(true);
  254. });
  255. openLastSelectedNode();
  256. });
  257. /***
  258. * 加载指定的文档到编辑器中
  259. * @param $node
  260. */
  261. window.loadDocument = function ($node) {
  262. var index = layer.load(1, {
  263. shade: [0.1, '#fff'] // 0.1 透明度的白色背景
  264. });
  265. $.get(window.editURL + $node.node.id).done(function (res) {
  266. layer.close(index);
  267. if (res.errcode === 0) {
  268. window.isLoad = true;
  269. try {
  270. window.editor.setTheme(res.data.markdown_theme);
  271. window.editor.setMarkdown(res.data.markdown);
  272. } catch (e) {
  273. console.log(e);
  274. }
  275. 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 };
  276. pushDocumentCategory(node);
  277. window.selectNode = node;
  278. pushVueLists(res.data.attach);
  279. setLastSelectNode($node);
  280. } else {
  281. layer.msg(editormdLocales[lang].loadDocFailed);
  282. }
  283. }).fail(function () {
  284. layer.close(index);
  285. layer.msg(editormdLocales[lang].loadDocFailed);
  286. });
  287. };
  288. /**
  289. * 保存文档到服务器
  290. * @param $is_cover 是否强制覆盖
  291. */
  292. function saveDocument($is_cover, callback) {
  293. var index = null;
  294. var node = window.selectNode;
  295. var content = window.editor.getMarkdown();
  296. var html = window.editor.getHtml(true);
  297. var markdownTheme = window.editor.getTheme();
  298. var version = "";
  299. if (!node) {
  300. layer.msg(editormdLocales[lang].fetchDocFailed);
  301. return;
  302. }
  303. if (node.a_attr && node.a_attr.disabled) {
  304. layer.msg(editormdLocales[lang].cannotAddToEmptyNode);
  305. return;
  306. }
  307. var doc_id = parseInt(node.id);
  308. for (var i in window.documentCategory) {
  309. var item = window.documentCategory[i];
  310. if (item.id === doc_id) {
  311. version = item.version;
  312. break;
  313. }
  314. }
  315. $.ajax({
  316. beforeSend: function () {
  317. index = layer.load(1, { shade: [0.1, '#fff'] });
  318. window.saveing = true;
  319. },
  320. url: window.editURL,
  321. data: { "identify": window.book.identify, "doc_id": doc_id, "markdown": content, "html": html, "markdown_theme": markdownTheme, "cover": $is_cover ? "yes" : "no", "version": version },
  322. type: "post",
  323. timeout: 30000,
  324. dataType: "json",
  325. success: function (res) {
  326. if (res.errcode === 0) {
  327. resetEditorChanged(false);
  328. for (var i in window.documentCategory) {
  329. var item = window.documentCategory[i];
  330. if (item.id === doc_id) {
  331. window.documentCategory[i].version = res.data.version;
  332. break;
  333. }
  334. }
  335. $.each(window.documentCategory, function (i, item) {
  336. var $item = window.documentCategory[i];
  337. if (item.id === doc_id) {
  338. window.documentCategory[i].version = res.data.version;
  339. }
  340. });
  341. if (typeof callback === "function") {
  342. callback();
  343. }
  344. } else if (res.errcode === 6005) {
  345. var confirmIndex = layer.confirm(editormdLocales[lang].overrideModified, {
  346. btn: [editormdLocales[lang].confirm, editormdLocales[lang].cancel] // 按钮
  347. }, function () {
  348. layer.close(confirmIndex);
  349. saveDocument(true, callback);
  350. });
  351. } else {
  352. layer.msg(res.message);
  353. }
  354. },
  355. error: function (XMLHttpRequest, textStatus, errorThrown) {
  356. layer.msg(window.editormdLocales[window.lang].serverExcept + errorThrown);
  357. },
  358. complete: function () {
  359. layer.close(index);
  360. window.saveing = false;
  361. }
  362. });
  363. }
  364. /**
  365. * 设置编辑器变更状态
  366. * @param $is_change
  367. */
  368. function resetEditorChanged($is_change) {
  369. if ($is_change && !window.isLoad) {
  370. $("#markdown-save").removeClass('disabled').addClass('change');
  371. } else {
  372. $("#markdown-save").removeClass('change').addClass('disabled');
  373. }
  374. window.isLoad = false;
  375. }
  376. /**
  377. * 返回上一个页面
  378. */
  379. function backWard() {
  380. if (document.referrer == "") { // 没有上一级
  381. var homepage = window.location.origin;
  382. window.location.href = homepage; // 返回首页
  383. return;
  384. }
  385. window.location.href = document.referrer;
  386. }
  387. /**
  388. * 发布文档
  389. */
  390. function releaseDocument() {
  391. if (Object.prototype.toString.call(window.documentCategory) === '[object Array]' && window.documentCategory.length > 0) {
  392. if ($("#markdown-save").hasClass('change')) {
  393. var confirm_result = confirm(editormdLocales[lang].contentUnsaved);
  394. if (confirm_result) {
  395. saveDocument(false, releaseBook);
  396. return;
  397. }
  398. }
  399. releaseBook();
  400. return
  401. }
  402. layer.msg(editormdLocales[lang].noDocNeedPublish)
  403. }
  404. /**
  405. * 显示/隐藏边栏
  406. */
  407. function siderChange() {
  408. $("#manualCategory").toggle(0, "swing", function () {
  409. var $then = $("#manualEditorContainer");
  410. var left = parseInt($then.css("left"));
  411. if (left > 0) {
  412. window.editorContainerLeft = left;
  413. $then.css("left", "0");
  414. } else {
  415. $then.css("left", window.editorContainerLeft + "px");
  416. }
  417. });
  418. }
  419. /**
  420. * 显示文档历史
  421. */
  422. function showHistory() {
  423. window.documentHistory();
  424. }
  425. /**
  426. * 添加文档
  427. */
  428. $("#addDocumentForm").ajaxForm({
  429. beforeSubmit: function () {
  430. var doc_name = $.trim($("#documentName").val());
  431. if (doc_name === "") {
  432. return showError(editormdLocales[lang].contentsNameEmpty, "#add-error-message")
  433. }
  434. $("#btnSaveDocument").button("loading");
  435. return true;
  436. },
  437. success: function (res) {
  438. if (res.errcode === 0) {
  439. var data = {
  440. "id": res.data.doc_id,
  441. 'parent': res.data.parent_id === 0 ? '#' : res.data.parent_id,
  442. "text": res.data.doc_name,
  443. "identify": res.data.identify,
  444. "version": res.data.version,
  445. state: { opened: res.data.is_open == 1 },
  446. a_attr: { is_open: res.data.is_open == 1 }
  447. };
  448. var node = window.treeCatalog.get_node(data.id);
  449. if (node) {
  450. window.treeCatalog.rename_node({ "id": data.id }, data.text);
  451. $("#sidebar").jstree(true).get_node(data.id).a_attr.is_open = data.state.opened;
  452. } else {
  453. window.treeCatalog.create_node(data.parent, data);
  454. window.treeCatalog.deselect_all();
  455. window.treeCatalog.select_node(data);
  456. }
  457. pushDocumentCategory(data);
  458. $("#markdown-save").removeClass('change').addClass('disabled');
  459. $("#addDocumentModal").modal('hide');
  460. } else {
  461. showError(res.message, "#add-error-message");
  462. }
  463. $("#btnSaveDocument").button("reset");
  464. }
  465. });
  466. /**
  467. * 文档目录树
  468. */
  469. $("#sidebar").jstree({
  470. 'plugins': ["wholerow", "types", 'dnd', 'contextmenu'],
  471. "types": {
  472. "default": {
  473. "icon": false // 删除默认图标
  474. }
  475. },
  476. 'core': {
  477. 'worker':true,
  478. 'check_callback': true,
  479. "multiple": false,
  480. 'animation': 0,
  481. "data": window.documentCategory
  482. },
  483. "contextmenu": {
  484. show_at_node: false,
  485. select_node: false,
  486. "items": {
  487. "添加文档": {
  488. "separator_before": false,
  489. "separator_after": true,
  490. "_disabled": false,
  491. "label": window.editormdLocales[window.lang].addDoc,//"添加文档",
  492. "icon": "fa fa-plus",
  493. "action": function (data) {
  494. var inst = $.jstree.reference(data.reference),
  495. node = inst.get_node(data.reference);
  496. openCreateCatalogDialog(node);
  497. }
  498. },
  499. "编辑": {
  500. "separator_before": false,
  501. "separator_after": true,
  502. "_disabled": false,
  503. "label": window.editormdLocales[window.lang].edit,
  504. "icon": "fa fa-edit",
  505. "action": function (data) {
  506. var inst = $.jstree.reference(data.reference);
  507. var node = inst.get_node(data.reference);
  508. openEditCatalogDialog(node);
  509. }
  510. },
  511. "删除": {
  512. "separator_before": false,
  513. "separator_after": true,
  514. "_disabled": false,
  515. "label": window.editormdLocales[window.lang].delete,
  516. "icon": "fa fa-trash-o",
  517. "action": function (data) {
  518. var inst = $.jstree.reference(data.reference);
  519. var node = inst.get_node(data.reference);
  520. openDeleteDocumentDialog(node);
  521. }
  522. }
  523. }
  524. }
  525. }).on("ready.jstree", function () {
  526. window.treeCatalog = $("#sidebar").jstree(true);
  527. //如果没有选中节点则选中默认节点
  528. // openLastSelectedNode();
  529. }).on('select_node.jstree', function (node, selected) {
  530. if ($("#markdown-save").hasClass('change')) {
  531. if (confirm(window.editormdLocales[window.lang].contentUnsaved)) {
  532. saveDocument(false, function () {
  533. loadDocument(selected);
  534. });
  535. return true;
  536. }
  537. }
  538. //如果是空目录则直接出发展开下一级功能
  539. if (selected.node.a_attr && selected.node.a_attr.disabled) {
  540. selected.instance.toggle_node(selected.node);
  541. return false
  542. }
  543. loadDocument(selected);
  544. }).on("move_node.jstree", jstree_save).on("delete_node.jstree", function ($node, $parent) {
  545. openLastSelectedNode();
  546. });
  547. /**
  548. * 打开文档模板
  549. */
  550. $("#documentTemplateModal").on("click", ".section>a[data-type]", function () {
  551. var $this = $(this).attr("data-type");
  552. if ($this === "customs") {
  553. $("#displayCustomsTemplateModal").modal("show");
  554. return;
  555. }
  556. var body = $("#template-" + $this).html();
  557. if (body) {
  558. window.isLoad = true;
  559. window.editor.clear();
  560. window.editor.insertValue(body);
  561. window.editor.setCursor({ line: 0, ch: 0 });
  562. resetEditorChanged(true);
  563. }
  564. $("#documentTemplateModal").modal('hide');
  565. });
  566. });
  567. function myFileUpload(file, callback) {
  568. // 创建 FormData 对象以便包含要上传的文件
  569. var formData = new FormData();
  570. formData.append("editormd-file-file", file); // "file" 是与你的服务端接口相对应的字段名
  571. var layerIndex = 0;
  572. // AJAX 请求
  573. $.ajax({
  574. url: window.fileUploadURL, // 确保此 URL 是文件上传 API 的正确 URL
  575. type: "POST",
  576. async: false, // 3xxx 20240609这里修改为同步,保证cherry批量上传图片时,插入的图片名称是正确的,否则,插入的图片名称都是最后一个名称
  577. dataType: "json",
  578. data: formData,
  579. processData: false, // 必须设置为 false,因为数据是 FormData 对象,不需要对数据进行序列化处理
  580. contentType: false, // 必须设置为 false,因为是 FormData 对象,jQuery 将不会设置内容类型头
  581. beforeSend: function () {
  582. layerIndex = layer.load(1, {
  583. shade: [0.1, '#fff'] // 0.1 透明度的白色背景
  584. });
  585. },
  586. error: function () {
  587. layer.close(layerIndex);
  588. layer.msg(locales[lang].uploadFailed);
  589. },
  590. success: function (data) {
  591. layer.close(layerIndex);
  592. // 验证data是否为数组
  593. if (data.errcode !== 0) {
  594. layer.msg(data.message);
  595. } else {
  596. callback(data.url); // 假设返回的 JSON 中包含上传文件的 URL,调用回调函数并传入 URL
  597. }
  598. }
  599. });
  600. }