u-parse.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  1. <template>
  2. <view id="_root" :class="(selectable?'_select ':'')+'_root'">
  3. <slot v-if="!nodes[0]"/>
  4. <!-- #ifndef APP-PLUS-NVUE -->
  5. <node v-else :childs="nodes" :opts="[lazyLoad,loadingImg,errorImg,showImgMenu]"/>
  6. <!-- #endif -->
  7. <!-- #ifdef APP-PLUS-NVUE -->
  8. <web-view ref="web" src="/static/app-plus/mp-html/local.html" :style="'margin-top:-2px;height:' + height + 'px'"
  9. @onPostMessage="_onMessage"/>
  10. <!-- #endif -->
  11. </view>
  12. </template>
  13. <script>
  14. import props from './props.js';
  15. // #ifndef APP-PLUS-NVUE
  16. import node from './node/node'
  17. /**
  18. * mp-html v2.0.4
  19. * @description 富文本组件
  20. * @tutorial https://github.com/jin-yufeng/mp-html
  21. * @property {String} bgColor 背景颜色,只适用与APP-PLUS-NVUE
  22. * @property {String} content 用于渲染的富文本字符串(默认 true )
  23. * @property {Boolean} copyLink 是否允许外部链接被点击时自动复制
  24. * @property {String} domain 主域名,用于拼接链接
  25. * @property {String} errorImg 图片出错时的占位图链接
  26. * @property {Boolean} lazyLoad 是否开启图片懒加载(默认 true )
  27. * @property {string} loadingImg 图片加载过程中的占位图链接
  28. * @property {Boolean} pauseVideo 是否在播放一个视频时自动暂停其它视频(默认 true )
  29. * @property {Boolean} previewImg 是否允许图片被点击时自动预览(默认 true )
  30. * @property {Boolean} scrollTable 是否给每个表格添加一个滚动层使其能单独横向滚动
  31. * @property {Boolean} selectable 是否开启长按复制
  32. * @property {Boolean} setTitle 是否将 title 标签的内容设置到页面标题(默认 true )
  33. * @property {Boolean} showImgMenu 是否允许图片被长按时显示菜单(默认 true )
  34. * @property {Object} tagStyle 标签的默认样式
  35. * @property {Boolean | Number} useAnchor 是否使用锚点链接
  36. *
  37. * @event {Function} load dom 结构加载完毕时触发
  38. * @event {Function} ready 所有图片加载完毕时触发
  39. * @event {Function} imgTap 图片被点击时触发
  40. * @event {Function} linkTap 链接被点击时触发
  41. * @event {Function} error 媒体加载出错时触发
  42. */
  43. const plugins = []
  44. const parser = require('./parser')
  45. // #endif
  46. // #ifdef APP-PLUS-NVUE
  47. const dom = weex.requireModule('dom')
  48. // #endif
  49. export default {
  50. name: 'mp-html',
  51. data() {
  52. return {
  53. nodes: [],
  54. // #ifdef APP-PLUS-NVUE
  55. height: 0
  56. // #endif
  57. }
  58. },
  59. mixins: [props],
  60. // #ifndef APP-PLUS-NVUE
  61. components: {
  62. node
  63. },
  64. // #endif
  65. watch: {
  66. content(content) {
  67. this.setContent(content)
  68. }
  69. },
  70. created() {
  71. this.plugins = []
  72. for (let i = plugins.length; i--;)
  73. this.plugins.push(new plugins[i](this))
  74. },
  75. mounted() {
  76. if (this.content && !this.nodes.length)
  77. this.setContent(this.content)
  78. },
  79. beforeDestroy() {
  80. this._hook('onDetached')
  81. clearInterval(this._timer)
  82. },
  83. methods: {
  84. /**
  85. * @description 将锚点跳转的范围限定在一个 scroll-view 内
  86. * @param {Object} page scroll-view 所在页面的示例
  87. * @param {String} selector scroll-view 的选择器
  88. * @param {String} scrollTop scroll-view scroll-top 属性绑定的变量名
  89. */
  90. in(page, selector, scrollTop) {
  91. // #ifndef APP-PLUS-NVUE
  92. if (page && selector && scrollTop)
  93. this._in = {
  94. page,
  95. selector,
  96. scrollTop
  97. }
  98. // #endif
  99. },
  100. /**
  101. * @description 锚点跳转
  102. * @param {String} id 要跳转的锚点 id
  103. * @param {Number} offset 跳转位置的偏移量
  104. * @returns {Promise}
  105. */
  106. navigateTo(id, offset) {
  107. return new Promise((resolve, reject) => {
  108. if (!this.useAnchor)
  109. return reject('Anchor is disabled')
  110. offset = offset || parseInt(this.useAnchor) || 0
  111. // #ifdef APP-PLUS-NVUE
  112. if (!id) {
  113. dom.scrollToElement(this.$refs.web, {
  114. offset
  115. })
  116. resolve()
  117. } else {
  118. this._navigateTo = {
  119. resolve,
  120. reject,
  121. offset
  122. }
  123. this.$refs.web.evalJs('uni.postMessage({data:{action:"getOffset",offset:(document.getElementById(' + id + ')||{}).offsetTop}})')
  124. }
  125. // #endif
  126. // #ifndef APP-PLUS-NVUE
  127. let deep = ' '
  128. // #ifdef MP-WEIXIN || MP-QQ || MP-TOUTIAO
  129. deep = '>>>'
  130. // #endif
  131. const selector = uni.createSelectorQuery()
  132. // #ifndef MP-ALIPAY
  133. .in(this._in ? this._in.page : this)
  134. // #endif
  135. .select((this._in ? this._in.selector : '._root') + (id ? `${deep}#${id}` : '')).boundingClientRect()
  136. if (this._in)
  137. selector.select(this._in.selector).scrollOffset()
  138. .select(this._in.selector).boundingClientRect() // 获取 scroll-view 的位置和滚动距离
  139. else
  140. selector.selectViewport().scrollOffset() // 获取窗口的滚动距离
  141. selector.exec(res => {
  142. if (!res[0])
  143. return reject('Label not found')
  144. const scrollTop = res[1].scrollTop + res[0].top - (res[2] ? res[2].top : 0) + offset
  145. if (this._in)
  146. // scroll-view 跳转
  147. this._in.page[this._in.scrollTop] = scrollTop
  148. else
  149. // 页面跳转
  150. uni.pageScrollTo({
  151. scrollTop,
  152. duration: 300
  153. })
  154. resolve()
  155. })
  156. // #endif
  157. })
  158. },
  159. /**
  160. * @description 获取文本内容
  161. * @return {String}
  162. */
  163. getText() {
  164. let text = '';
  165. (function traversal(nodes) {
  166. for (let i = 0; i < nodes.length; i++) {
  167. const node = nodes[i]
  168. if (node.type == 'text')
  169. text += node.text.replace(/&amp;/g, '&')
  170. else if (node.name == 'br')
  171. text += '\n'
  172. else {
  173. // 块级标签前后加换行
  174. const isBlock = node.name == 'p' || node.name == 'div' || node.name == 'tr' || node.name == 'li' || (node.name[0] == 'h' && node.name[1] > '0' && node.name[1] < '7')
  175. if (isBlock && text && text[text.length - 1] != '\n')
  176. text += '\n'
  177. // 递归获取子节点的文本
  178. if (node.children)
  179. traversal(node.children)
  180. if (isBlock && text[text.length - 1] != '\n')
  181. text += '\n'
  182. else if (node.name == 'td' || node.name == 'th')
  183. text += '\t'
  184. }
  185. }
  186. })(this.nodes)
  187. return text
  188. },
  189. /**
  190. * @description 获取内容大小和位置
  191. * @return {Promise}
  192. */
  193. getRect() {
  194. return new Promise((resolve, reject) => {
  195. uni.createSelectorQuery()
  196. // #ifndef MP-ALIPAY
  197. .in(this)
  198. // #endif
  199. .select('#_root').boundingClientRect().exec(res => res[0] ? resolve(res[0]) : reject('Root label not found'))
  200. })
  201. },
  202. /**
  203. * @description 设置内容
  204. * @param {String} content html 内容
  205. * @param {Boolean} append 是否在尾部追加
  206. */
  207. setContent(content, append) {
  208. if (!append || !this.imgList)
  209. this.imgList = []
  210. const nodes = new parser(this).parse(content)
  211. // #ifdef APP-PLUS-NVUE
  212. if (this._ready)
  213. this._set(nodes, append)
  214. // #endif
  215. this.$set(this, 'nodes', append ? (this.nodes || []).concat(nodes) : nodes)
  216. // #ifndef APP-PLUS-NVUE
  217. this._videos = []
  218. this.$nextTick(() => {
  219. this._hook('onLoad')
  220. this.$emit('load')
  221. })
  222. // 等待图片加载完毕
  223. let height
  224. clearInterval(this._timer)
  225. this._timer = setInterval(() => {
  226. this.getRect().then(rect => {
  227. // 350ms 总高度无变化就触发 ready 事件
  228. if (rect.height == height) {
  229. this.$emit('ready', rect)
  230. clearInterval(this._timer)
  231. }
  232. height = rect.height
  233. }).catch(() => {
  234. })
  235. }, 350)
  236. // #endif
  237. },
  238. /**
  239. * @description 调用插件钩子函数
  240. */
  241. _hook(name) {
  242. for (let i = plugins.length; i--;)
  243. if (this.plugins[i][name])
  244. this.plugins[i][name]()
  245. },
  246. // #ifdef APP-PLUS-NVUE
  247. /**
  248. * @description 设置内容
  249. */
  250. _set(nodes, append) {
  251. this.$refs.web.evalJs('setContent(' + JSON.stringify(nodes) + ',' + JSON.stringify([this.bgColor, this.errorImg, this.loadingImg, this.pauseVideo, this.scrollTable, this.selectable]) + ',' + append + ')')
  252. },
  253. /**
  254. * @description 接收到 web-view 消息
  255. */
  256. _onMessage(e) {
  257. const message = e.detail.data[0]
  258. switch (message.action) {
  259. // web-view 初始化完毕
  260. case 'onJSBridgeReady':
  261. this._ready = true
  262. if (this.nodes)
  263. this._set(this.nodes)
  264. break
  265. // 内容 dom 加载完毕
  266. case 'onLoad':
  267. this.height = message.height
  268. this._hook('onLoad')
  269. this.$emit('load')
  270. break
  271. // 所有图片加载完毕
  272. case 'onReady':
  273. this.getRect().then(res => {
  274. this.$emit('ready', res)
  275. }).catch(() => {
  276. })
  277. break
  278. // 总高度发生变化
  279. case 'onHeightChange':
  280. this.height = message.height
  281. break
  282. // 图片点击
  283. case 'onImgTap':
  284. this.$emit('imgTap', message.attrs)
  285. if (this.previewImg)
  286. uni.previewImage({
  287. current: parseInt(message.attrs.i),
  288. urls: this.imgList
  289. })
  290. break
  291. // 链接点击
  292. case 'onLinkTap':
  293. const href = message.attrs.href
  294. this.$emit('linkTap', message.attrs)
  295. if (href) {
  296. // 锚点跳转
  297. if (href[0] == '#') {
  298. if (this.useAnchor)
  299. dom.scrollToElement(this.$refs.web, {
  300. offset: message.offset
  301. })
  302. }
  303. // 打开外链
  304. else if (href.includes('://')) {
  305. if (this.copyLink)
  306. plus.runtime.openWeb(href)
  307. } else
  308. uni.navigateTo({
  309. url: href,
  310. fail() {
  311. wx.switchTab({
  312. url: href
  313. })
  314. }
  315. })
  316. }
  317. break
  318. // 获取到锚点的偏移量
  319. case 'getOffset':
  320. if (typeof message.offset == 'number') {
  321. dom.scrollToElement(this.$refs.web, {
  322. offset: message.offset + this._navigateTo.offset
  323. })
  324. this._navigateTo.resolve()
  325. } else
  326. this._navigateTo.reject('Label not found')
  327. break
  328. // 点击
  329. case 'onClick':
  330. this.$emit('tap')
  331. break
  332. // 出错
  333. case 'onError':
  334. this.$emit('error', {
  335. source: message.source,
  336. attrs: message.attrs
  337. })
  338. }
  339. }
  340. // #endif
  341. }
  342. }
  343. </script>
  344. <style>
  345. /* #ifndef APP-PLUS-NVUE */
  346. /* 根节点样式 */
  347. ._root {
  348. overflow: auto;
  349. -webkit-overflow-scrolling: touch;
  350. }
  351. /* 长按复制 */
  352. ._select {
  353. user-select: text;
  354. }
  355. /* #endif */
  356. </style>