index.js 12 KB

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