index.js 13 KB

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