index.js 13 KB

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