index.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  1. /**
  2. * @fileoverview editable 插件
  3. */
  4. const config = require('./config')
  5. const Parser = require('../parser')
  6. function Editable (vm) {
  7. this.vm = vm
  8. this.editHistory = [] // 历史记录
  9. this.editI = -1 // 历史记录指针
  10. vm._mask = [] // 蒙版被点击时进行的操作
  11. vm._setData = function (path, val) {
  12. const paths = path.split('.')
  13. let target = vm
  14. for (let i = 0; i < paths.length - 1; i++) {
  15. target = target[paths[i]]
  16. }
  17. vm.$set(target, paths.pop(), val)
  18. }
  19. /**
  20. * @description 移动历史记录指针
  21. * @param {Number} num 移动距离
  22. */
  23. const move = num => {
  24. setTimeout(() => {
  25. const item = this.editHistory[this.editI + num]
  26. if (item) {
  27. this.editI += num
  28. vm._setData(item.key, item.value)
  29. }
  30. }, 200)
  31. }
  32. vm.undo = () => move(-1) // 撤销
  33. vm.redo = () => move(1) // 重做
  34. /**
  35. * @description 更新记录
  36. * @param {String} path 更新内容路径
  37. * @param {*} oldVal 旧值
  38. * @param {*} newVal 新值
  39. * @param {Boolean} set 是否更新到视图
  40. * @private
  41. */
  42. vm._editVal = (path, oldVal, newVal, set) => {
  43. // 当前指针后的内容去除
  44. while (this.editI < this.editHistory.length - 1) {
  45. this.editHistory.pop()
  46. }
  47. // 最多存储 30 条操作记录
  48. while (this.editHistory.length > 30) {
  49. this.editHistory.pop()
  50. this.editI--
  51. }
  52. const last = this.editHistory[this.editHistory.length - 1]
  53. if (!last || last.key !== path) {
  54. if (last) {
  55. // 去掉上一次的新值
  56. this.editHistory.pop()
  57. this.editI--
  58. }
  59. // 存入这一次的旧值
  60. this.editHistory.push({
  61. key: path,
  62. value: oldVal
  63. })
  64. this.editI++
  65. }
  66. // 存入本次的新值
  67. this.editHistory.push({
  68. key: path,
  69. value: newVal
  70. })
  71. this.editI++
  72. // 更新到视图
  73. if (set) {
  74. vm._setData(path, newVal)
  75. }
  76. }
  77. /**
  78. * @description 获取菜单项
  79. * @private
  80. */
  81. vm._getItem = function (node, up, down) {
  82. let items
  83. let i
  84. if (node === 'color') {
  85. return config.color
  86. }
  87. if (node.name === 'img') {
  88. items = config.img.slice(0)
  89. if (!vm.getSrc) {
  90. i = items.indexOf('换图')
  91. if (i !== -1) {
  92. items.splice(i, 1)
  93. }
  94. i = items.indexOf('超链接')
  95. if (i !== -1) {
  96. items.splice(i, 1)
  97. }
  98. i = items.indexOf('预览图')
  99. if (i !== -1) {
  100. items.splice(i, 1)
  101. }
  102. }
  103. i = items.indexOf('禁用预览')
  104. if (i !== -1 && node.attrs.ignore) {
  105. items[i] = '启用预览'
  106. }
  107. } else if (node.name === 'a') {
  108. items = config.link.slice(0)
  109. if (!vm.getSrc) {
  110. i = items.indexOf('更换链接')
  111. if (i !== -1) {
  112. items.splice(i, 1)
  113. }
  114. }
  115. } else if (node.name === 'video' || node.name === 'audio') {
  116. items = config.media.slice(0)
  117. i = items.indexOf('封面')
  118. if (!vm.getSrc && i !== -1) {
  119. items.splice(i, 1)
  120. }
  121. i = items.indexOf('循环')
  122. if (node.attrs.loop && i !== -1) {
  123. items[i] = '不循环'
  124. }
  125. i = items.indexOf('自动播放')
  126. if (node.attrs.autoplay && i !== -1) {
  127. items[i] = '不自动播放'
  128. }
  129. } else if (node.name === 'card') {
  130. items = config.card.slice(0)
  131. } else {
  132. items = config.node.slice(0)
  133. }
  134. if (!up) {
  135. i = items.indexOf('上移')
  136. if (i !== -1) {
  137. items.splice(i, 1)
  138. }
  139. }
  140. if (!down) {
  141. i = items.indexOf('下移')
  142. if (i !== -1) {
  143. items.splice(i, 1)
  144. }
  145. }
  146. return items
  147. }
  148. /**
  149. * @description 显示 tooltip
  150. * @param {object} obj
  151. * @private
  152. */
  153. vm._tooltip = function (obj) {
  154. vm.$set(vm, 'tooltip', {
  155. top: obj.top,
  156. items: obj.items
  157. })
  158. vm._tooltipcb = obj.success
  159. }
  160. /**
  161. * @description 显示滚动条
  162. * @param {object} obj
  163. * @private
  164. */
  165. vm._slider = function (obj) {
  166. vm.$set(vm, 'slider', {
  167. min: obj.min,
  168. max: obj.max,
  169. value: obj.value,
  170. top: obj.top
  171. })
  172. vm._slideringcb = obj.changing
  173. vm._slidercb = obj.change
  174. }
  175. /**
  176. * @description 显示颜色选择
  177. * @param {object} obj
  178. * @private
  179. */
  180. vm._color = function (obj) {
  181. vm.$set(vm, 'color', {
  182. items: obj.items,
  183. top: obj.top
  184. })
  185. vm._colorcb = obj.success
  186. }
  187. /**
  188. * @description 点击蒙版
  189. * @private
  190. */
  191. vm._maskTap = function () {
  192. // 隐藏所有悬浮窗
  193. while (vm._mask.length) {
  194. (vm._mask.pop())()
  195. }
  196. if (vm.tooltip) {
  197. vm.$set(vm, 'tooltip', null)
  198. }
  199. if (vm.slider) {
  200. vm.$set(vm, 'slider', null)
  201. }
  202. if (vm.color) {
  203. vm.$set(vm, 'color', null)
  204. }
  205. }
  206. /**
  207. * @description 插入节点
  208. * @param {Object} node
  209. */
  210. function insert (node) {
  211. if (vm._edit) {
  212. vm._edit.insert(node)
  213. } else {
  214. const nodes = vm.nodes.slice(0)
  215. nodes.push(node)
  216. vm._editVal('nodes', vm.nodes, nodes, true)
  217. }
  218. }
  219. /**
  220. * @description 在光标处插入指定 html 内容
  221. * @param {String} html 内容
  222. */
  223. vm.insertHtml = html => {
  224. this.inserting = true
  225. const arr = new Parser(vm).parse(html)
  226. this.inserting = undefined
  227. for (let i = 0; i < arr.length; i++) {
  228. insert(arr[i])
  229. }
  230. }
  231. /**
  232. * @description 在光标处插入图片
  233. */
  234. vm.insertImg = function () {
  235. vm.getSrc && vm.getSrc('img').then(src => {
  236. if (typeof src === 'string') {
  237. src = [src]
  238. }
  239. const parser = new Parser(vm)
  240. for (let i = 0; i < src.length; i++) {
  241. insert({
  242. name: 'img',
  243. attrs: {
  244. src: parser.getUrl(src[i])
  245. }
  246. })
  247. }
  248. }).catch(() => { })
  249. }
  250. /**
  251. * @description 在光标处插入一个链接
  252. */
  253. vm.insertLink = function () {
  254. vm.getSrc && vm.getSrc('link').then(url => {
  255. insert({
  256. name: 'a',
  257. attrs: {
  258. href: url
  259. },
  260. children: [{
  261. type: 'text',
  262. text: url
  263. }]
  264. })
  265. }).catch(() => { })
  266. }
  267. /**
  268. * @description 在光标处插入一个表格
  269. * @param {Number} rows 行数
  270. * @param {Number} cols 列数
  271. */
  272. vm.insertTable = function (rows, cols) {
  273. const table = {
  274. name: 'table',
  275. attrs: {
  276. style: 'display:table;width:100%;margin:10px 0;text-align:center;border-spacing:0;border-collapse:collapse;border:1px solid gray'
  277. },
  278. children: []
  279. }
  280. for (let i = 0; i < rows; i++) {
  281. const tr = {
  282. name: 'tr',
  283. attrs: {},
  284. children: []
  285. }
  286. for (let j = 0; j < cols; j++) {
  287. tr.children.push({
  288. name: 'td',
  289. attrs: {
  290. style: 'padding:2px;border:1px solid gray'
  291. },
  292. children: [{
  293. type: 'text',
  294. text: ''
  295. }]
  296. })
  297. }
  298. table.children.push(tr)
  299. }
  300. insert(table)
  301. }
  302. /**
  303. * @description 插入视频/音频
  304. * @param {Object} node
  305. */
  306. function insertMedia (node) {
  307. if (typeof node.src === 'string') {
  308. node.src = [node.src]
  309. }
  310. const parser = new Parser(vm)
  311. // 拼接主域名
  312. for (let i = 0; i < node.src.length; i++) {
  313. node.src[i] = parser.getUrl(node.src[i])
  314. }
  315. insert({
  316. name: 'div',
  317. attrs: {
  318. style: 'text-align:center'
  319. },
  320. children: [node]
  321. })
  322. }
  323. /**
  324. * @description 在光标处插入一个视频
  325. */
  326. vm.insertVideo = function () {
  327. vm.getSrc && vm.getSrc('video').then(src => {
  328. insertMedia({
  329. name: 'video',
  330. attrs: {
  331. controls: 'T'
  332. },
  333. children: [],
  334. src,
  335. // #ifdef APP-PLUS
  336. html: `<video src="${src}" style="width:100%;height:100%"></video>`
  337. // #endif
  338. })
  339. }).catch(() => { })
  340. }
  341. /**
  342. * @description 在光标处插入一个音频
  343. */
  344. vm.insertAudio = function () {
  345. vm.getSrc && vm.getSrc('audio').then(attrs => {
  346. let src
  347. if (attrs.src) {
  348. src = attrs.src
  349. attrs.src = undefined
  350. } else {
  351. src = attrs
  352. attrs = {}
  353. }
  354. attrs.controls = 'T'
  355. insertMedia({
  356. name: 'audio',
  357. attrs,
  358. children: [],
  359. src
  360. })
  361. }).catch(() => { })
  362. }
  363. /**
  364. * @description 在光标处插入一段文本
  365. */
  366. vm.insertText = function () {
  367. insert({
  368. name: 'p',
  369. attrs: {},
  370. children: [{
  371. type: 'text',
  372. text: ''
  373. }]
  374. })
  375. }
  376. /**
  377. * @description 清空内容
  378. */
  379. vm.clear = function () {
  380. vm._maskTap()
  381. vm._edit = undefined
  382. vm.$set(vm, 'nodes', [{
  383. name: 'p',
  384. attrs: {},
  385. children: [{
  386. type: 'text',
  387. text: ''
  388. }]
  389. }])
  390. }
  391. /**
  392. * @description 获取编辑后的 html
  393. */
  394. vm.getContent = function () {
  395. let html = '';
  396. // 递归遍历获取
  397. (function traversal (nodes, table) {
  398. for (let i = 0; i < nodes.length; i++) {
  399. let item = nodes[i]
  400. if (item.type === 'text') {
  401. html += item.text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\n/g, '<br>').replace(/\xa0/g, '&nbsp;') // 编码实体
  402. } else {
  403. if (item.name === 'img') {
  404. item.attrs.i = ''
  405. // 还原被转换的 svg
  406. if ((item.attrs.src || '').includes('data:image/svg+xml;utf8,')) {
  407. html += item.attrs.src.substr(24).replace(/%23/g, '#').replace('<svg', '<svg style="' + (item.attrs.style || '') + '"')
  408. continue
  409. }
  410. } else if (item.name === 'video' || item.name === 'audio') {
  411. // 还原 video 和 audio 的 source
  412. item = JSON.parse(JSON.stringify(item))
  413. if (item.src.length > 1) {
  414. item.children = []
  415. for (let j = 0; j < item.src.length; j++) {
  416. item.children.push({
  417. name: 'source',
  418. attrs: {
  419. src: item.src[j]
  420. }
  421. })
  422. }
  423. } else {
  424. item.attrs.src = item.src[0]
  425. }
  426. } else if (item.name === 'div' && (item.attrs.style || '').includes('overflow:auto') && (item.children[0] || {}).name === 'table') {
  427. // 还原滚动层
  428. item = item.children[0]
  429. }
  430. // 还原 table
  431. if (item.name === 'table') {
  432. item = JSON.parse(JSON.stringify(item))
  433. table = item.attrs
  434. if ((item.attrs.style || '').includes('display:grid')) {
  435. item.attrs.style = item.attrs.style.split('display:grid')[0]
  436. const children = [{
  437. name: 'tr',
  438. attrs: {},
  439. children: []
  440. }]
  441. for (let j = 0; j < item.children.length; j++) {
  442. item.children[j].attrs.style = item.children[j].attrs.style.replace(/grid-[^;]+;*/g, '')
  443. if (item.children[j].r !== children.length) {
  444. children.push({
  445. name: 'tr',
  446. attrs: {},
  447. children: [item.children[j]]
  448. })
  449. } else {
  450. children[children.length - 1].children.push(item.children[j])
  451. }
  452. }
  453. item.children = children
  454. }
  455. }
  456. html += '<' + item.name
  457. for (const attr in item.attrs) {
  458. let val = item.attrs[attr]
  459. if (!val) continue
  460. if (val === 'T' || val === true) {
  461. // bool 型省略值
  462. html += ' ' + attr
  463. continue
  464. } else if (item.name[0] === 't' && attr === 'style' && table) {
  465. // 取消为了显示 table 添加的 style
  466. val = val.replace(/;*display:table[^;]*/, '')
  467. if (table.border) {
  468. val = val.replace(/border[^;]+;*/g, $ => $.includes('collapse') ? $ : '')
  469. }
  470. if (table.cellpadding) {
  471. val = val.replace(/padding[^;]+;*/g, '')
  472. }
  473. if (!val) continue
  474. }
  475. html += ' ' + attr + '="' + val.replace(/"/g, '&quot;') + '"'
  476. }
  477. html += '>'
  478. if (item.children) {
  479. traversal(item.children, table)
  480. html += '</' + item.name + '>'
  481. }
  482. }
  483. }
  484. })(vm.nodes)
  485. // 其他插件处理
  486. for (let i = vm.plugins.length; i--;) {
  487. if (vm.plugins[i].onGetContent) {
  488. html = vm.plugins[i].onGetContent(html) || html
  489. }
  490. }
  491. return html
  492. }
  493. }
  494. Editable.prototype.onUpdate = function (content, config) {
  495. if (this.vm.editable) {
  496. this.vm._maskTap()
  497. config.entities.amp = '&'
  498. if (!this.inserting) {
  499. this.vm._edit = undefined
  500. if (!content) {
  501. setTimeout(() => {
  502. this.vm.$set(this.vm, 'nodes', [{
  503. name: 'p',
  504. attrs: {},
  505. children: [{
  506. type: 'text',
  507. text: ''
  508. }]
  509. }])
  510. }, 0)
  511. }
  512. }
  513. }
  514. }
  515. Editable.prototype.onParse = function (node) {
  516. // 空白单元格可编辑
  517. if (this.vm.editable && (node.name === 'td' || node.name === 'th') && !this.vm.getText(node.children)) {
  518. node.children.push({
  519. type: 'text',
  520. text: ''
  521. })
  522. }
  523. }
  524. module.exports = Editable