grapes.init.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500
  1. var faCogActive = true;
  2. var faThLargeActive = false;
  3. var activeIcon = getVariable('active_icon');
  4. if (activeIcon) {
  5. faCogActive = activeIcon === 'fa-cog'? true : false;
  6. faThLargeActive = activeIcon === 'fa-th-large'? true : false;
  7. }
  8. const panelsConfig = {
  9. stylePrefix: 'pn-', // 面板的样式前缀
  10. defaults: [
  11. {
  12. id: 'commands',
  13. buttons: [{}],
  14. },
  15. {
  16. id: 'options',
  17. buttons: [
  18. {
  19. active: false,
  20. id: 'sw-visibility',
  21. className: 'fa fa-square-o',
  22. command: 'core:component-outline',
  23. context: 'sw-visibility',
  24. attributes: { title: 'View components' },
  25. },
  26. {
  27. id: 'preview',
  28. className: 'fa fa-eye',
  29. command: 'preview',
  30. context: 'preview',
  31. attributes: { title: 'Preview' },
  32. },
  33. {
  34. id: 'fullscreen',
  35. className: 'fa fa-arrows-alt',
  36. command: 'fullscreen',
  37. context: 'fullscreen',
  38. attributes: { title: 'Fullscreen' },
  39. },
  40. // {
  41. // id: 'export-template',
  42. // className: 'fa fa-code',
  43. // command: 'export-template',
  44. // attributes: { title: 'View code' },
  45. // },
  46. ],
  47. },
  48. {
  49. id: 'views',
  50. buttons: [
  51. // {
  52. // id: 'open-sm',
  53. // className: 'fa fa-paint-brush',
  54. // command: 'open-sm',
  55. // active: false,
  56. // togglable: false,
  57. // attributes: { title: 'Open Style Manager' },
  58. // },
  59. {
  60. id: 'open-tm',
  61. className: 'fa fa-cog',
  62. command: 'open-tm',
  63. active: faCogActive,
  64. togglable: true,
  65. attributes: { title: 'Settings' },
  66. },
  67. // {
  68. // id: 'open-layers',
  69. // className: 'fa fa-bars',
  70. // command: 'open-layers',
  71. // togglable: false,
  72. // attributes: { title: 'Open Layer Manager' },
  73. // },
  74. // {
  75. // id: 'open-blocks',
  76. // className: 'fa fa-th-large',
  77. // command: 'open-blocks',
  78. // active: faThLargeActive,
  79. // togglable: true,
  80. // attributes: { title: 'Open Blocks' },
  81. // },
  82. ],
  83. },
  84. ],
  85. };
  86. // 定义自定义组件类型
  87. const myNewComponentTypes = (editor) => {
  88. //连接属性
  89. // editor.Components.addType('a', {
  90. // isComponent: (el) => el.tagName === 'A',
  91. // model: {
  92. // defaults: {
  93. // traits: [
  94. // {
  95. // type: 'text', // Type of the trait
  96. // name: 'href', // (required) The name of the attribute/property to use on component
  97. // label: 'URL', // The label you will see in Settings
  98. // }
  99. // ],
  100. // },
  101. // },
  102. // });
  103. //图片属性
  104. // editor.Components.addType('img', {
  105. // isComponent: (el) => el.tagName === 'IMG',
  106. // model: {
  107. // defaults: {
  108. // traits: [
  109. // {
  110. // type: 'text', // Type of the trait
  111. // name: 'title', // (required) The name of the attribute/property to use on component
  112. // label: 'title', // The label you will see in Settings
  113. // },
  114. // {
  115. // type: 'text', // Type of the trait
  116. // name: 'width', // (required) The name of the attribute/property to use on component
  117. // label: 'width', // The label you will see in Settings
  118. // },
  119. // {
  120. // type: 'text', // Type of the trait
  121. // name: 'height', // (required) The name of the attribute/property to use on component
  122. // label: 'height', // The label you will see in Settings
  123. // },
  124. // ],
  125. // },
  126. // },
  127. // });
  128. };
  129. const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
  130. // 创建 GrapesJS 编辑器实例
  131. const editor = grapesjs.init({
  132. container: '#mtb_visual_editor', // 指定编辑器容器的 DOM 元素
  133. fromElement: true, // 从 DOM 元素中加载初始内容
  134. height: '100%', // 设置编辑器的高度
  135. width: '100%', // 设置宽度自适应
  136. storageManager: false,
  137. panels: panelsConfig,
  138. plugins: [myNewComponentTypes],
  139. assetManager: {
  140. upload: '/dist/visual-editor/upload',
  141. uploadName: '_file_',
  142. multiUpload:false,
  143. headers: {
  144. 'X-CSRF-TOKEN': csrfToken,
  145. },
  146. },
  147. });
  148. //定义block块
  149. const bm = editor.Blocks; // `Blocks` is an alias of `BlockManager`
  150. bm.add('block_p', {
  151. // Your block properties...
  152. label: 'Paragraph',
  153. media: `<svg style="width:20px;height:20px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M192 32l64 0 160 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-32 0 0 352c0 17.7-14.3 32-32 32s-32-14.3-32-32l0-352-32 0 0 352c0 17.7-14.3 32-32 32s-32-14.3-32-32l0-96-32 0c-88.4 0-160-71.6-160-160s71.6-160 160-160z"/></svg>`,
  154. content: '<p>insert your paragraph here</p>',
  155. });
  156. bm.add('block_span', {
  157. // Your block properties...
  158. label: 'Text',
  159. media: `<svg style="width:20px;height:20px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M32 32C14.3 32 0 46.3 0 64S14.3 96 32 96l128 0 0 352c0 17.7 14.3 32 32 32s32-14.3 32-32l0-352 128 0c17.7 0 32-14.3 32-32s-14.3-32-32-32L192 32 32 32z"/></svg>`,
  160. content: '<span>insert your text here</span>',
  161. });
  162. //图片管理器
  163. const am = editor.AssetManager;
  164. // 锁定整个画布
  165. editor.getWrapper().set({ locked: true });
  166. // 遍历所有组件并解锁具有edit属性的组件
  167. // const edit_classes = ['mtb_edit']; // 定义需要解锁的组件类名
  168. editor.getComponents().each(component => {
  169. component.onAll(component => {
  170. //解销CLASS
  171. // let html = component.toHTML();
  172. // const classes = component.getClasses();
  173. // // 判断是否有交集
  174. // const hasIntersection = classes.some(item => {
  175. // // 如果是数组,则递归检查子数组
  176. // if (Array.isArray(item)) {
  177. // return item.some(subItem => edit_classes.includes(subItem));
  178. // }
  179. // // 如果是字符串,直接检查是否在 edit_classes 中
  180. // return edit_classes.includes(item);
  181. // });
  182. // 如果有交集,解锁组件
  183. if (component.get('attributes').mtb_edit == '1' || component.get('attributes').mtb_edit == 'true') {
  184. component.set({ locked: false });
  185. setBorder(component);
  186. }
  187. if (component.get('attributes').mtb_edit == '0' || component.get('attributes').mtb_edit == 'false') {
  188. component.set({ locked: true });
  189. }
  190. if (component.get('attributes').mtb_edit == '2') {
  191. component.set({ locked: false });
  192. setBorder(component);
  193. //关闭子组件
  194. component.forEachChild(child => {
  195. child.set({ locked: true });
  196. })
  197. }
  198. })
  199. });
  200. function setBorder(component) {
  201. if (component.attributes.tagName == 'a') {
  202. component.setStyle({
  203. border: '2px dotted #f08300'
  204. });
  205. } else {
  206. component.setStyle({
  207. border: '1px dotted #3b97e3'
  208. });
  209. }
  210. }
  211. // 监听组件选择事件
  212. editor.on('component:selected', (comp) => {
  213. //去掉选中组件后的按钮
  214. const selectedComponent = editor.getSelected();
  215. //const defaultToolbar = selectedComponent.get('toolbar');
  216. //先不给删除按钮
  217. if (selectedComponent.get('attributes').mtb_edit == 'true' || selectedComponent.get('attributes').mtb_edit == '1' || selectedComponent.get('attributes').mtb_edit == '2') {
  218. selectedComponent.set({
  219. toolbar: [
  220. ]
  221. });
  222. } else {
  223. selectedComponent.set({
  224. toolbar: [
  225. ]
  226. // toolbar: [ ...defaultToolbar, { attributes: {class: commandIcon}, command: commandToAdd}]
  227. // toolbar: [{
  228. // attributes: { class: 'fa fa-clone' },
  229. // command: 'tlb-clone',
  230. // title: 'Clone',
  231. // }, {
  232. // attributes: { class: 'fa fa-trash' },
  233. // command: 'tlb-delete',
  234. // title: 'Delete',
  235. // }]
  236. });
  237. }
  238. if (selectedComponent.get('attributes').mtb_toolbar != '' && selectedComponent.get('attributes').mtb_toolbar != undefined) {
  239. const mtbToolbar = selectedComponent.get('attributes').mtb_toolbar;
  240. const safeActions = mtbToolbar.split(',');
  241. console.log(safeActions);
  242. let inputToolbar = [];
  243. if (safeActions.includes('clone')) {
  244. // 执行克隆相关逻辑
  245. inputToolbar.push({
  246. attributes: { class: 'fa fa-clone' },
  247. command: 'tlb-clone',
  248. title: 'Clone',
  249. });
  250. }
  251. if (safeActions.includes('delete')) {
  252. // 执行删除相关逻辑
  253. inputToolbar.push({
  254. attributes: { class: 'fa fa-trash' },
  255. command: 'tlb-delete',
  256. title: 'Delete',
  257. });
  258. }
  259. selectedComponent.set({
  260. toolbar: inputToolbar
  261. });
  262. }
  263. // 双击图片时弹出图片管理器
  264. if (selectedComponent.attributes.tagName == 'img') {
  265. //双击图片时弹出图片管理器
  266. const el = selectedComponent.getEl();
  267. // 添加双击事件监听器
  268. el.addEventListener('dblclick', () => {
  269. am.open({
  270. types: ['image'], // This is the default option
  271. select(asset, complete) {
  272. const selected = editor.getSelected();
  273. if (selected) {
  274. selected.addAttributes({ src: asset.getSrc() });
  275. complete && am.close();
  276. }
  277. },
  278. });
  279. });
  280. }
  281. //其他
  282. });
  283. // 隐藏或显示左侧面板
  284. function leftPanelDisplay(isShow = true) {
  285. if (isShow) {
  286. let elements1 = document.querySelectorAll('.gjs-pn-views-container');
  287. elements1.forEach(function(element) {
  288. element.style.display = 'block'; // 或者 'flex',取决于你希望如何显示这些元素
  289. });
  290. let elements2 = document.querySelectorAll('.gjs-pn-views');
  291. elements2.forEach(function(element) {
  292. element.style = 'border_bottom: 2px solid var(--gjs-main-dark-color)';
  293. });
  294. } else {
  295. let elements1 = document.querySelectorAll('.gjs-pn-views-container');
  296. elements1.forEach(function(element) {
  297. element.style.display = 'none'; // 或者 'flex',取决于你希望如何显示这些元素
  298. });
  299. let elements2 = document.querySelectorAll('.gjs-pn-views');
  300. elements2.forEach(function(element) {
  301. element.style = 'border-bottom:none';
  302. });
  303. }
  304. }
  305. // 监听编辑器加载完成事件
  306. editor.on('load', () => {
  307. //----------
  308. // 下接菜单
  309. //----------
  310. // 获取目标元素
  311. const targetElement = document.querySelector('.fa-arrows-alt');
  312. // 创建下拉选择框
  313. const dropdown = document.createElement('select');
  314. dropdown.className = 'custom-select'; // 添加自定义样式类
  315. dropdown.id = 'dropdown'; // 添加 id 属性
  316. // 构建 option 标签
  317. var options = '';
  318. siteMenu.forEach(function (item) {
  319. if (mid == item.id) {
  320. options += `<option value="${item.id}" uri="${item.url}" selected>${item.title}</option>`;
  321. } else {
  322. options += `<option value="${item.id}" uri="${item.url}">${item.title}</option>`;
  323. }
  324. });
  325. // 将生成的 option 标签添加到 dropdown 中
  326. dropdown.innerHTML = options;
  327. // 将下拉选择框插入到目标元素之后
  328. targetElement.insertAdjacentElement('afterend', dropdown);
  329. dropdownel = document.getElementById('dropdown');
  330. // 监听 change 事件
  331. dropdownel.addEventListener('change', function () {
  332. // 获取选中的 option
  333. var selectedOption = dropdownel.options[dropdownel.selectedIndex];
  334. // 获取选中的 id 和 uri
  335. var id = selectedOption.value;
  336. var uri = selectedOption.getAttribute('uri');
  337. if (id > 0) {
  338. // 生成跳转的 URL
  339. var redirectUrl = `?mid=${id}&uri=${encodeURIComponent(uri)}`;
  340. // 跳转到生成的 URL
  341. window.location.href = redirectUrl;
  342. }
  343. });
  344. //----------
  345. //发布
  346. //----------
  347. customSelect = document.querySelector('.custom-select');
  348. // 创建 sendButton 按钮
  349. const sendButton = document.createElement('button');
  350. sendButton.textContent = publishBtnName; // 按钮文本
  351. sendButton.classList.add('send-button');
  352. sendButton.classList.add('layui-btn');
  353. sendButton.classList.add('layui-btn-radius');
  354. sendButton.classList.add('layui-btn-radius');
  355. sendButton.style = 'margin-left:10px;height:25px;line-height:25px;padding:0 10px;vertical-align:unset;'; // 添加样式(可选)
  356. customSelect.insertAdjacentElement('afterend', sendButton);
  357. //发布按钮点击事件
  358. sendButton.addEventListener('click', function () {
  359. // 显示loading
  360. loadIndex = layer.load(0, {shade: [0.5, '#000']});
  361. // 发送数据
  362. $.ajax({
  363. url: '/dist/visual-editor/publish',
  364. type: 'POST',
  365. headers: {
  366. 'X-CSRF-TOKEN': csrfToken,
  367. },
  368. data: {},
  369. success: function (res) {
  370. layer.close(loadIndex);
  371. layer.msg(res.message,{shade: [0.5, '#000']});
  372. },
  373. error: function (err) {
  374. layer.close(loadIndex);
  375. layer.msg(err,{shade: [0.5, '#000']});
  376. }
  377. });
  378. });
  379. //----------
  380. // 右侧面板显示隐藏
  381. //----------
  382. // 监听属性按钮点击事件
  383. var gjsPnBtn = document.querySelectorAll('.gjs-pn-btn');
  384. var headRightBtn = ['fa-cog', 'fa-th-large'];
  385. gjsPnBtn.forEach(function(icon1) {
  386. let hasIntersection = headRightBtn.some(function(btnClass) {
  387. return icon1.classList.contains(btnClass);
  388. });
  389. if (hasIntersection) {
  390. icon1.addEventListener('click', function() {
  391. hasActive = false;
  392. gjsPnBtn.forEach(function(icon) {
  393. if (icon.classList.contains('gjs-pn-active')) {
  394. hasActive = true;
  395. saveVariable('active_icon',icon.classList.item(2));
  396. }
  397. });
  398. if (hasActive) {
  399. leftPanelDisplay(true);
  400. } else {
  401. leftPanelDisplay(false);
  402. saveVariable('active_icon','none');
  403. }
  404. });
  405. }
  406. });
  407. //右侧面板显示隐藏
  408. if (faCogActive === false && faThLargeActive === false) {
  409. leftPanelDisplay(false)
  410. }
  411. //----------
  412. //关闭loading
  413. //----------
  414. layer.close(loadIndex);
  415. $('body').css({
  416. visibility: 'visible',
  417. opacity: 1
  418. });
  419. });
  420. //保存模版内容
  421. editor.on('update', () => {
  422. const html = editor.getHtml();
  423. // 你可以在这里添加自定义逻辑
  424. //ajax提交数据
  425. $.ajax({
  426. url: '/dist/visual-editor/preview-save',
  427. type: 'POST',
  428. headers: {
  429. 'X-CSRF-TOKEN': csrfToken,
  430. },
  431. data: { // 发送数据
  432. html: html
  433. },
  434. success: function (res) {
  435. if (res.status != 1) {
  436. layer.msg('save data error, please try again later!',{shade: [0.5, '#000']});
  437. }
  438. },
  439. error: function (err) {
  440. alert(err);
  441. }
  442. });
  443. });
  444. // 上传图片时触发loading效果
  445. editor.on('asset:upload:start', () => {
  446. loadIndex = layer.load(0, {shade: [0.5, '#000']});
  447. });
  448. editor.on('asset:upload:end', () => {
  449. layer.close(loadIndex);
  450. });
  451. // 图片懒加载
  452. layui.use('flow', function(){
  453. var flow = layui.flow;
  454. //当你执行这样一个方法时,即对页面中的全部带有 lay-src 的 img 元素开启了懒加载(当然你也可以指定相关 img)
  455. flow.lazyimg();
  456. });
  457. // 保存变量到LocalStorage
  458. function saveVariable(name, value) {
  459. localStorage.setItem(name, JSON.stringify(value));
  460. }
  461. // 从LocalStorage读取变量
  462. function getVariable(name) {
  463. const value = localStorage.getItem(name);
  464. return value ? JSON.parse(value) : null;
  465. }