Initial commit
This commit is contained in:
396
wechat-mini-program/components/mp-html/src/miniprogram/index.js
Normal file
396
wechat-mini-program/components/mp-html/src/miniprogram/index.js
Normal file
@@ -0,0 +1,396 @@
|
||||
/*!
|
||||
* mp-html v2.5.1
|
||||
* https://github.com/jin-yufeng/mp-html
|
||||
*
|
||||
* Released under the MIT license
|
||||
* Author: Jin Yufeng
|
||||
*/
|
||||
const Parser = require('./parser')
|
||||
const plugins = []
|
||||
|
||||
Component({
|
||||
data: {
|
||||
nodes: []
|
||||
},
|
||||
properties: {
|
||||
/**
|
||||
* @description 容器的样式
|
||||
* @type {String}
|
||||
*/
|
||||
containerStyle: String,
|
||||
|
||||
/**
|
||||
* @description 用于渲染的 html 字符串
|
||||
* @type {String}
|
||||
*/
|
||||
content: {
|
||||
type: String,
|
||||
value: '',
|
||||
observer (content) {
|
||||
this.setContent(content)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 是否允许外部链接被点击时自动复制
|
||||
* @type {Boolean}
|
||||
* @default true
|
||||
*/
|
||||
copyLink: {
|
||||
type: Boolean,
|
||||
value: true
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 主域名,用于拼接链接
|
||||
* @type {String}
|
||||
*/
|
||||
domain: String,
|
||||
|
||||
/**
|
||||
* @description 图片出错时的占位图链接
|
||||
* @type {String}
|
||||
*/
|
||||
errorImg: String,
|
||||
|
||||
/**
|
||||
* @description 是否开启图片懒加载
|
||||
* @type {Boolean}
|
||||
* @default false
|
||||
*/
|
||||
lazyLoad: Boolean,
|
||||
|
||||
/**
|
||||
* @description 图片加载过程中的占位图链接
|
||||
* @type {String}
|
||||
*/
|
||||
loadingImg: String,
|
||||
|
||||
/**
|
||||
* @description 是否在播放一个视频时自动暂停其他视频
|
||||
* @type {Boolean}
|
||||
* @default true
|
||||
*/
|
||||
pauseVideo: {
|
||||
type: Boolean,
|
||||
value: true
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 是否允许图片被点击时自动预览
|
||||
* @type {Boolean | String}
|
||||
* @default true
|
||||
*/
|
||||
previewImg: {
|
||||
type: null,
|
||||
value: true
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 是否给每个表格添加一个滚动层使其能单独横向滚动
|
||||
* @type {Boolean}
|
||||
* @default false
|
||||
*/
|
||||
scrollTable: Boolean,
|
||||
|
||||
/**
|
||||
* @description 是否开启长按复制
|
||||
* @type {Boolean | String}
|
||||
* @default false
|
||||
*/
|
||||
selectable: null,
|
||||
|
||||
/**
|
||||
* @description 是否将 title 标签的内容设置到页面标题
|
||||
* @type {Boolean}
|
||||
* @default true
|
||||
*/
|
||||
setTitle: {
|
||||
type: Boolean,
|
||||
value: true
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 是否允许图片被长按时显示菜单
|
||||
* @type {Boolean}
|
||||
* @default true
|
||||
*/
|
||||
showImgMenu: {
|
||||
type: Boolean,
|
||||
value: true
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 标签的默认样式
|
||||
* @type {Object}
|
||||
*/
|
||||
tagStyle: Object,
|
||||
|
||||
/**
|
||||
* @description 是否使用锚点链接
|
||||
* @type {Boolean | Number}
|
||||
* @default false
|
||||
*/
|
||||
useAnchor: null
|
||||
},
|
||||
|
||||
created () {
|
||||
this.plugins = []
|
||||
for (let i = plugins.length; i--;) {
|
||||
this.plugins.push(new plugins[i](this))
|
||||
}
|
||||
|
||||
// #ifdef MP-ALIPAY
|
||||
if (this.properties.content) {
|
||||
this.setContent(this.properties.content)
|
||||
}
|
||||
// #endif
|
||||
},
|
||||
|
||||
// #ifdef MP-ALIPAY
|
||||
didUpdate (e) {
|
||||
if (e.content !== this.properties.content) {
|
||||
this.setContent(this.properties.content)
|
||||
}
|
||||
},
|
||||
// #endif
|
||||
|
||||
detached () {
|
||||
// 注销插件
|
||||
this._hook('onDetached')
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* @description 将锚点跳转的范围限定在一个 scroll-view 内
|
||||
* @param {Object} page scroll-view 所在页面的示例
|
||||
* @param {String} selector scroll-view 的选择器
|
||||
* @param {String} scrollTop scroll-view scroll-top 属性绑定的变量名
|
||||
*/
|
||||
in (page, selector, scrollTop) {
|
||||
if (page && selector && scrollTop) {
|
||||
this._in = {
|
||||
page,
|
||||
selector,
|
||||
scrollTop
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 锚点跳转
|
||||
* @param {String} id 要跳转的锚点 id
|
||||
* @param {Number} offset 跳转位置的偏移量
|
||||
* @returns {Promise}
|
||||
*/
|
||||
navigateTo (id, offset) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.properties.useAnchor) {
|
||||
reject(Error('Anchor is disabled'))
|
||||
return
|
||||
}
|
||||
// 跨组件选择器
|
||||
const deep =
|
||||
// #ifdef MP-WEIXIN || MP-QQ || MP-TOUTIAO
|
||||
'>>>'
|
||||
// #endif
|
||||
// #ifdef MP-BAIDU || MP-ALIPAY
|
||||
' ' // eslint-disable-line
|
||||
// #endif
|
||||
const selector = wx.createSelectorQuery()
|
||||
// #ifndef MP-ALIPAY
|
||||
.in(this._in ? this._in.page : this)
|
||||
// #endif
|
||||
.select((this._in ? this._in.selector : '._root') + (id ? `${deep}#${id}` : '')).boundingClientRect()
|
||||
if (this._in) {
|
||||
selector.select(this._in.selector).scrollOffset()
|
||||
.select(this._in.selector).boundingClientRect()
|
||||
} else {
|
||||
// 获取 scroll-view 的位置和滚动距离
|
||||
selector.selectViewport().scrollOffset() // 获取窗口的滚动距离
|
||||
}
|
||||
selector.exec(res => {
|
||||
if (!res[0]) {
|
||||
reject(Error('Label not found'))
|
||||
return
|
||||
}
|
||||
const scrollTop = res[1].scrollTop + res[0].top - (res[2] ? res[2].top : 0) + (offset || parseInt(this.properties.useAnchor) || 0)
|
||||
if (this._in) {
|
||||
// scroll-view 跳转
|
||||
this._in.page.setData({
|
||||
[this._in.scrollTop]: scrollTop
|
||||
})
|
||||
} else {
|
||||
// 页面跳转
|
||||
wx.pageScrollTo({
|
||||
scrollTop,
|
||||
duration: 300
|
||||
})
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 获取文本内容
|
||||
* @returns {String}
|
||||
*/
|
||||
getText (nodes) {
|
||||
let text = '';
|
||||
(function traversal (nodes) {
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i]
|
||||
if (node.type === 'text') {
|
||||
text += node.text.replace(/&/g, '&')
|
||||
} else if (node.name === 'br') {
|
||||
text += '\n'
|
||||
} else {
|
||||
// 块级标签前后加换行
|
||||
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')
|
||||
if (isBlock && text && text[text.length - 1] !== '\n') {
|
||||
text += '\n'
|
||||
}
|
||||
// 递归获取子节点的文本
|
||||
if (node.children) {
|
||||
traversal(node.children)
|
||||
}
|
||||
if (isBlock && text[text.length - 1] !== '\n') {
|
||||
text += '\n'
|
||||
} else if (node.name === 'td' || node.name === 'th') {
|
||||
text += '\t'
|
||||
}
|
||||
}
|
||||
}
|
||||
})(nodes || this.data.nodes)
|
||||
return text
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 获取内容大小
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getRect () {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.createSelectorQuery()
|
||||
// #ifndef MP-ALIPAY
|
||||
.in(this)
|
||||
// #endif
|
||||
.select('._root').boundingClientRect().exec(res => res[0] ? resolve(res[0]) : reject(Error('Root label not found')))
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 暂停播放媒体
|
||||
*/
|
||||
pauseMedia () {
|
||||
for (let i = (this._videos || []).length; i--;) {
|
||||
this._videos[i].pause()
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 设置媒体播放速率
|
||||
* @param {Number} rate 播放速率
|
||||
*/
|
||||
setPlaybackRate (rate) {
|
||||
this.playbackRate = rate
|
||||
for (let i = (this._videos || []).length; i--;) {
|
||||
this._videos[i].playbackRate(rate)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 设置富文本内容
|
||||
* @param {string} content 要渲染的 html 字符串
|
||||
* @param {boolean} append 是否在尾部追加
|
||||
*/
|
||||
setContent (content, append) {
|
||||
if (!this.imgList || !append) {
|
||||
this.imgList = []
|
||||
}
|
||||
this._videos = []
|
||||
|
||||
const data = {}
|
||||
const nodes = new Parser(this).parse(content)
|
||||
// 尾部追加内容
|
||||
if (append) {
|
||||
for (let i = this.data.nodes.length, j = nodes.length; j--;) {
|
||||
data[`nodes[${i + j}]`] = nodes[j]
|
||||
}
|
||||
} else {
|
||||
data.nodes = nodes
|
||||
}
|
||||
|
||||
this.setData(data,
|
||||
// #ifndef MP-TOUTIAO
|
||||
() => {
|
||||
this._hook('onLoad')
|
||||
this.triggerEvent('load')
|
||||
}
|
||||
// #endif
|
||||
)
|
||||
|
||||
// #ifdef MP-TOUTIAO
|
||||
this.selectComponent('#_root', child => {
|
||||
child.root = this
|
||||
this._hook('onLoad')
|
||||
this.triggerEvent('load')
|
||||
})
|
||||
// #endif
|
||||
|
||||
if (this.properties.lazyLoad || this.imgList._unloadimgs < this.imgList.length / 2) {
|
||||
// 设置懒加载,每 350ms 获取高度,不变则认为加载完毕
|
||||
let height = 0
|
||||
const callback = rect => {
|
||||
if (!rect || !rect.height) rect = {}
|
||||
// 350ms 总高度无变化就触发 ready 事件
|
||||
if (rect.height === height) {
|
||||
this.triggerEvent('ready', rect)
|
||||
} else {
|
||||
height = rect.height
|
||||
setTimeout(() => {
|
||||
this.getRect().then(callback).catch(callback)
|
||||
}, 350)
|
||||
}
|
||||
}
|
||||
this.getRect().then(callback).catch(callback)
|
||||
} else {
|
||||
// 未设置懒加载,等待所有图片加载完毕
|
||||
if (!this.imgList._unloadimgs) {
|
||||
this.getRect().then(rect => {
|
||||
this.triggerEvent('ready', rect)
|
||||
}).catch(() => {
|
||||
this.triggerEvent('ready', {})
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 调用插件的钩子函数
|
||||
* @private
|
||||
*/
|
||||
_hook (name) {
|
||||
for (let i = plugins.length; i--;) {
|
||||
if (this.plugins[i][name]) {
|
||||
this.plugins[i][name]()
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// #ifndef MP-TOUTIAO
|
||||
/**
|
||||
* @description 添加子组件
|
||||
* @private
|
||||
*/
|
||||
_add (e) {
|
||||
e
|
||||
// #ifndef MP-ALIPAY
|
||||
.detail
|
||||
// #endif
|
||||
.root = this
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {
|
||||
"node": "./node/node"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<!-- 主组件 -->
|
||||
<view class="_root {{selectable?'_select':''}}" style="{{containerStyle}}">
|
||||
<!-- 加载完成前显示自定义 loading -->
|
||||
<slot wx:if="{{!nodes[0]}}" />
|
||||
<!-- 节点树 -->
|
||||
<node id="_root" childs="{{nodes}}" opts="{{[lazyLoad,loadingImg,errorImg,showImgMenu,selectable]}}" mp-weixin:mp-qq:mp-baidu:catchadd="_add" mp-alipay:onAdd="_add" />
|
||||
</view>
|
||||
@@ -0,0 +1,13 @@
|
||||
/* 根节点样式 */
|
||||
._root {
|
||||
padding: 1px 0;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
/* 长按复制 */
|
||||
._select {
|
||||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
/**
|
||||
* @fileoverview 递归子组件,用于显示节点树
|
||||
*/
|
||||
Component({
|
||||
data: {
|
||||
ctrl: {}, // 控制信号
|
||||
// #ifdef MP-WEIXIN
|
||||
isiOS: wx.getSystemInfoSync().system.includes('iOS')
|
||||
// #endif
|
||||
},
|
||||
properties: {
|
||||
childs: Array, // 子节点列表
|
||||
opts: Array // 设置 [是否开启懒加载, 加载中占位图, 错误占位图, 是否使用长按菜单]
|
||||
},
|
||||
options: {
|
||||
addGlobalClass: true
|
||||
},
|
||||
// #ifndef MP-TOUTIAO
|
||||
attached () {
|
||||
// #ifndef MP-ALIPAY
|
||||
this.triggerEvent('add', this, {
|
||||
bubbles: true,
|
||||
composed: true
|
||||
})
|
||||
// #endif
|
||||
// #ifdef MP-ALIPAY
|
||||
this.props.onAdd(this)
|
||||
// #endif
|
||||
},
|
||||
// #endif
|
||||
methods: {
|
||||
noop () { },
|
||||
/**
|
||||
* @description 获取标签
|
||||
* @param {String} path 路径
|
||||
*/
|
||||
getNode (path) {
|
||||
try {
|
||||
const nums = path.split('_')
|
||||
let node = this.properties.childs[nums[0]]
|
||||
for (let i = 1; i < nums.length; i++) {
|
||||
node = node.children[nums[i]]
|
||||
}
|
||||
return node
|
||||
} catch {
|
||||
return {
|
||||
text: '',
|
||||
attrs: {},
|
||||
children: []
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @description 播放视频事件
|
||||
* @param {Event} e
|
||||
*/
|
||||
play (e) {
|
||||
const i = e.target.dataset.i
|
||||
const node = this.getNode(i)
|
||||
this.root.triggerEvent('play', {
|
||||
source: node.name,
|
||||
attrs: {
|
||||
...node.attrs,
|
||||
src: node.src[this.data.ctrl[i] || 0]
|
||||
}
|
||||
})
|
||||
if (this.root.properties.pauseVideo) {
|
||||
let flag = false
|
||||
const id = e.target.id
|
||||
for (let i = this.root._videos.length; i--;) {
|
||||
if (this.root._videos[i].id === id) {
|
||||
flag = true
|
||||
} else {
|
||||
this.root._videos[i].pause() // 自动暂停其他视频
|
||||
}
|
||||
}
|
||||
// 将自己加入列表
|
||||
if (!flag) {
|
||||
const ctx = wx.createVideoContext(id
|
||||
// #ifndef MP-BAIDU
|
||||
, this
|
||||
// #endif
|
||||
)
|
||||
ctx.id = id
|
||||
if (this.root.playbackRate) {
|
||||
ctx.playbackRate(this.root.playbackRate)
|
||||
}
|
||||
this.root._videos.push(ctx)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 图片点击事件
|
||||
* @param {Event} e
|
||||
*/
|
||||
imgTap (e) {
|
||||
const node = this.getNode(e.target.dataset.i)
|
||||
// 父级中有链接
|
||||
if (node.a) return this.linkTap(node.a)
|
||||
if (node.attrs.ignore) return
|
||||
this.root.triggerEvent('imgtap', node.attrs)
|
||||
if (this.root.properties.previewImg) {
|
||||
const current =
|
||||
// #ifndef MP-ALIPAY
|
||||
this.root.imgList[node.i]
|
||||
// #endif
|
||||
// #ifdef MP-ALIPAY
|
||||
node.i // eslint-disable-line
|
||||
// #endif
|
||||
// 自动预览图片
|
||||
wx.previewImage({
|
||||
// #ifdef MP-WEIXIN
|
||||
showmenu: this.root.properties.showImgMenu,
|
||||
// #endif
|
||||
// #ifdef MP-ALIPAY
|
||||
enablesavephoto: this.root.properties.showImgMenu,
|
||||
enableShowPhotoDownload: this.root.properties.showImgMenu,
|
||||
// #endif
|
||||
current,
|
||||
urls: this.root.imgList
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 图片加载完成事件
|
||||
* @param {Event} e
|
||||
*/
|
||||
imgLoad (e) {
|
||||
const i = e.target.dataset.i
|
||||
const node = this.getNode(i)
|
||||
let val
|
||||
if (!node.w) {
|
||||
val = e.detail.width
|
||||
} else if ((this.properties.opts[1] && !this.data.ctrl[i]) || this.data.ctrl[i] === -1) {
|
||||
// 加载完毕,取消加载中占位图
|
||||
val = 1
|
||||
}
|
||||
if (val
|
||||
// #ifdef MP-TOUTIAO
|
||||
&& val !== this.data.ctrl[i] // eslint-disable-line
|
||||
// #endif
|
||||
) {
|
||||
this.setData({
|
||||
['ctrl.' + i]: val
|
||||
})
|
||||
}
|
||||
this.checkReady()
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 检查是否所有图片加载完毕
|
||||
*/
|
||||
checkReady () {
|
||||
if (!this.root.properties.lazyLoad) {
|
||||
this.root.imgList._unloadimgs -= 1
|
||||
if (!this.root.imgList._unloadimgs) {
|
||||
setTimeout(() => {
|
||||
this.root.getRect().then(rect => {
|
||||
this.root.triggerEvent('ready', rect)
|
||||
}).catch(() => {
|
||||
this.root.triggerEvent('ready', {})
|
||||
})
|
||||
}, 350)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 链接点击事件
|
||||
* @param {Event} e
|
||||
*/
|
||||
linkTap (e) {
|
||||
const node = e.currentTarget ? this.getNode(e.currentTarget.dataset.i) : {}
|
||||
const attrs = node.attrs || e
|
||||
const href = attrs.href
|
||||
this.root.triggerEvent('linktap', Object.assign({
|
||||
innerText: this.root.getText(node.children || []) // 链接内的文本内容
|
||||
}, attrs))
|
||||
if (href) {
|
||||
if (href[0] === '#') {
|
||||
// 跳转锚点
|
||||
this.root.navigateTo(href.substring(1)).catch(() => { })
|
||||
} else if (href.split('?')[0].includes('://')) {
|
||||
// 复制外部链接
|
||||
if (this.root.properties.copyLink) {
|
||||
wx.setClipboardData({
|
||||
data: href,
|
||||
success: () =>
|
||||
wx.showToast({
|
||||
title: '链接已复制'
|
||||
})
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// 跳转页面
|
||||
wx.navigateTo({
|
||||
url: href,
|
||||
fail () {
|
||||
wx.switchTab({
|
||||
url: href,
|
||||
fail () { }
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 错误事件
|
||||
* @param {Event} e
|
||||
*/
|
||||
mediaError (e) {
|
||||
const i = e.target.dataset.i
|
||||
const node = this.getNode(i)
|
||||
if (node.name === 'video' || node.name === 'audio') {
|
||||
// 加载其他源
|
||||
let index = (this.data.ctrl[i] || 0) + 1
|
||||
if (index > node.src.length) {
|
||||
index = 0
|
||||
}
|
||||
if (index < node.src.length) {
|
||||
return this.setData({
|
||||
['ctrl.' + i]: index
|
||||
})
|
||||
}
|
||||
} else if (node.name === 'img') {
|
||||
// 显示错误占位图
|
||||
if (this.properties.opts[2]) {
|
||||
this.setData({
|
||||
['ctrl.' + i]: -1
|
||||
})
|
||||
}
|
||||
this.checkReady()
|
||||
}
|
||||
if (this.root) {
|
||||
this.root.triggerEvent('error', {
|
||||
source: node.name,
|
||||
attrs: node.attrs,
|
||||
errMsg: e.detail.errMsg
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {
|
||||
"node": "./node"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
<!-- node 递归子组件 -->
|
||||
<!-- #ifdef MP-WEIXIN || MP-QQ -->
|
||||
<wxs module="isInline">
|
||||
// 行内标签列表
|
||||
var inlineTags = {
|
||||
abbr: true,
|
||||
b: true,
|
||||
big: true,
|
||||
code: true,
|
||||
del: true,
|
||||
em: true,
|
||||
i: true,
|
||||
ins: true,
|
||||
label: true,
|
||||
q: true,
|
||||
small: true,
|
||||
span: true,
|
||||
strong: true,
|
||||
sub: true,
|
||||
sup: true
|
||||
}
|
||||
/**
|
||||
* @description 判断是否为行内标签
|
||||
*/
|
||||
module.exports = function (tagName, style) {
|
||||
return inlineTags[tagName] || (style || '').indexOf('inline') !== -1
|
||||
}
|
||||
</wxs>
|
||||
<!-- #endif -->
|
||||
<template name="el">
|
||||
<!-- 图片 -->
|
||||
<block wx:if="{{n.name==='img'}}">
|
||||
<!-- 表格中的图片,使用 rich-text 防止大小不正确 -->
|
||||
<rich-text wx:if="{{n.t}}" style="display:{{n.t}}" nodes="<img class='_img' style='{{n.attrs.style}}' src='{{n.attrs.src}}'>" data-i="{{i}}" catchtap="imgTap" />
|
||||
<block wx:else>
|
||||
<!-- 占位图 -->
|
||||
<image wx:if="{{(opts[1]&&!ctrl[i])||ctrl[i]<0}}" class="_img" style="{{n.attrs.style}}" src="{{ctrl[i]<0?opts[2]:opts[1]}}" mode="widthFix" />
|
||||
<!-- 显示图片 -->
|
||||
<image id="{{n.attrs.id}}" class="_img {{n.attrs.class}}" style="{{ctrl[i]===-1?'display:none;':''}}width:{{ctrl[i]||1}}px;height:1px;{{n.attrs.style}}" src="{{n.attrs.src}}" mode="{{!n.h?'widthFix':(!n.w?'heightFix':(n.m||'scaleToFill'))}}" lazy-load="{{opts[0]}}" mp-weixin:mp-baidu:webp="{{n.webp}}" mp-weixin:show-menu-by-longpress="{{opts[3]&&!n.attrs.ignore}}" mp-baidu:image-menu-prevent="{{!opts[3]||n.attrs.ignore}}" data-i="{{i}}" bindload="imgLoad" binderror="mediaError" catchtap="imgTap" bindlongpress="noop" />
|
||||
</block>
|
||||
</block>
|
||||
<!-- 文本 -->
|
||||
<!-- #ifdef MP-WEIXIN || MP-QQ || MP-TOUTIAO -->
|
||||
<text wx:elif="{{n.text}}" mp-weixin:user-select="{{opts[4]=='force'&&isiOS}}" decode>{{n.text}}</text>
|
||||
<!-- #endif -->
|
||||
<text wx:elif="{{n.name==='br'}}">{{'\n'}}</text>
|
||||
<!-- 链接 -->
|
||||
<view wx:elif="{{n.name==='a'}}" id="{{n.attrs.id}}" class="{{n.attrs.href?'_a ':''}}{{n.attrs.class}}" hover-class="_hover" style="display:inline;{{n.attrs.style}}" data-i="{{i}}" catchtap="linkTap">
|
||||
<!-- #ifdef MP-WEIXIN || MP-QQ -->
|
||||
<node childs="{{n.children}}" opts="{{opts}}" style="display:inherit" />
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef MP-BAIDU -->
|
||||
<block wx:for="{{n.children}}" wx:key="index">
|
||||
<template is="el" data="{{n:item,i:i+'_'+index,opts:opts,ctrl:ctrl}}"></template>
|
||||
</block>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef MP-ALIPAY || MP-TOUTIAO -->
|
||||
<template is="node" data="{{childs:n.children,path:i+'_',opts:opts,ctrl:ctrl}}"></template>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
<!-- 视频 -->
|
||||
<video wx:elif="{{n.name==='video'}}" id="{{n.attrs.id}}" class="{{n.attrs.class}}" style="{{n.attrs.style}}" autoplay="{{n.attrs.autoplay}}" controls="{{n.attrs.controls}}" loop="{{n.attrs.loop}}" muted="{{n.attrs.muted}}" object-fit="{{n.attrs['object-fit']}}" poster="{{n.attrs.poster}}" src="{{n.src[ctrl[i]||0]}}" data-i="{{i}}" bindplay="play" binderror="mediaError" />
|
||||
<!-- #ifndef MP-TOUTIAO -->
|
||||
<!-- 音频 -->
|
||||
<audio wx:elif="{{n.name==='audio'}}" id="{{n.attrs.id}}" class="{{n.attrs.class}}" style="{{n.attrs.style}}" author="{{n.attrs.author}}" controls="{{n.attrs.controls}}" loop="{{n.attrs.loop}}" name="{{n.attrs.name}}" poster="{{n.attrs.poster}}" src="{{n.src[ctrl[i]||0]}}" data-i="{{i}}" bindplay="play" binderror="mediaError" />
|
||||
<!-- #endif -->
|
||||
<!-- insert -->
|
||||
<!-- 富文本 -->
|
||||
<rich-text wx:else id="{{n.attrs.id}}" mp-weixin:mp-qq:mp-baidu:mp-toutiao:style="{{n.f}}" mp-alipay:style="display:inline;{{n.f}}" mp-baidu:selectable="{{opts[4]}}" mp-weixin:user-select="{{opts[4]}}" nodes="{{[n]}}" />
|
||||
</template>
|
||||
<!-- #ifdef MP-ALIPAY || MP-TOUTIAO -->
|
||||
<template name="node">
|
||||
<block wx:for="{{childs}}" wx:for-item="n" wx:for-index="i" wx:key="i">
|
||||
<template wx:if="{{!n.c}}" is="el" data="{{n:n,i:path+i,opts:opts,ctrl:ctrl}}" />
|
||||
<view wx:else id="{{n.attrs.id}}" class="_{{n.name}} {{n.attrs.class}}" style="{{n.attrs.style}}">
|
||||
<template is="node" data="{{childs:n.children,path:path+i+'_',opts:opts,ctrl:ctrl}}"></template>
|
||||
</view>
|
||||
</block>
|
||||
</template>
|
||||
<template is="node" data="{{childs:childs,path:'',opts:opts,ctrl:ctrl}}"></template>
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef MP-ALIPAY || MP-TOUTIAO -->
|
||||
<!-- 第 1 层 -->
|
||||
<block wx:for="{{childs}}" wx:for-item="n1" wx:for-index="i1" wx:key="i1">
|
||||
<template mp-weixin:mp-qq:wx:if="{{!n1.c&&(!n1.children||n1.name==='a'||!isInline(n1.name,n1.attrs.style))}}" mp-baidu:wx:if="{{!n1.c}}" is="el" data="{{n:n1,i:''+i1,opts:opts,ctrl:ctrl}}" />
|
||||
<view wx:else id="{{n1.attrs.id}}" class="_{{n1.name}} {{n1.attrs.class}}" style="{{n1.attrs.style}}">
|
||||
<!-- 第 2 层 -->
|
||||
<block wx:for="{{n1.children}}" wx:for-item="n2" wx:for-index="i2" wx:key="i2">
|
||||
<template mp-weixin:mp-qq:wx:if="{{!n2.c&&(!n2.children||n2.name==='a'||!isInline(n2.name,n2.attrs.style))}}" mp-baidu:wx:if="{{!n2.c}}" is="el" data="{{n:n2,i:i1+'_'+i2,opts:opts,ctrl:ctrl}}" />
|
||||
<view wx:else id="{{n2.attrs.id}}" class="_{{n2.name}} {{n2.attrs.class}}" style="{{n2.attrs.style}}">
|
||||
<!-- 第 3 层 -->
|
||||
<block wx:for="{{n2.children}}" wx:for-item="n3" wx:for-index="i3" wx:key="i3">
|
||||
<template mp-weixin:mp-qq:wx:if="{{!n3.c&&(!n3.children||n3.name==='a'||!isInline(n3.name,n3.attrs.style))}}" mp-baidu:wx:if="{{!n3.c}}" is="el" data="{{n:n3,i:i1+'_'+i2+'_'+i3,opts:opts,ctrl:ctrl}}" />
|
||||
<view wx:else id="{{n3.attrs.id}}" class="_{{n3.name}} {{n3.attrs.class}}" style="{{n3.attrs.style}}">
|
||||
<!-- 第 4 层 -->
|
||||
<block wx:for="{{n3.children}}" wx:for-item="n4" wx:for-index="i4" wx:key="i4">
|
||||
<template mp-weixin:mp-qq:wx:if="{{!n4.c&&(!n4.children||n4.name==='a'||!isInline(n4.name,n4.attrs.style))}}" mp-baidu:wx:if="{{!n4.c}}" is="el" data="{{n:n4,i:i1+'_'+i2+'_'+i3+'_'+i4,opts:opts,ctrl:ctrl}}" />
|
||||
<view wx:else id="{{n4.attrs.id}}" class="_{{n4.name}} {{n4.attrs.class}}" style="{{n4.attrs.style}}">
|
||||
<!-- 第 5 层 -->
|
||||
<block wx:for="{{n4.children}}" wx:for-item="n5" wx:for-index="i5" wx:key="i5">
|
||||
<template mp-weixin:mp-qq:wx:if="{{!n5.c&&(!n5.children||n5.name==='a'||!isInline(n5.name,n5.attrs.style))}}" mp-baidu:wx:if="{{!n5.c}}" is="el" data="{{n:n5,i:i1+'_'+i2+'_'+i3+'_'+i4+'_'+i5,opts:opts,ctrl:ctrl}}" />
|
||||
<node wx:else id="{{n5.attrs.id}}" class="_{{n5.name}} {{n5.attrs.class}}" style="{{n5.attrs.style}}" childs="{{n5.children}}" opts="{{opts}}" />
|
||||
</block>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
</block>
|
||||
<!-- #endif -->
|
||||
@@ -0,0 +1,164 @@
|
||||
/* a 标签默认效果 */
|
||||
._a {
|
||||
padding: 1.5px 0 1.5px 0;
|
||||
color: #366092;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* a 标签点击态效果 */
|
||||
._hover {
|
||||
text-decoration: underline;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* 图片默认效果 */
|
||||
._img {
|
||||
max-width: 100%;
|
||||
-webkit-touch-callout: none;
|
||||
}
|
||||
|
||||
/* 内部样式 */
|
||||
|
||||
._b,
|
||||
._strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
._code {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
._del {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
._em,
|
||||
._i {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
._h1 {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
._h2 {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
._h3 {
|
||||
font-size: 1.17em;
|
||||
}
|
||||
|
||||
._h5 {
|
||||
font-size: 0.83em;
|
||||
}
|
||||
|
||||
._h6 {
|
||||
font-size: 0.67em;
|
||||
}
|
||||
|
||||
._h1,
|
||||
._h2,
|
||||
._h3,
|
||||
._h4,
|
||||
._h5,
|
||||
._h6 {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
._ins {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
._li {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
._ol {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
|
||||
._ol,
|
||||
._ul {
|
||||
display: block;
|
||||
padding-left: 40px;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
._q::before {
|
||||
content: '"';
|
||||
}
|
||||
|
||||
._q::after {
|
||||
content: '"';
|
||||
}
|
||||
|
||||
._sub {
|
||||
font-size: smaller;
|
||||
vertical-align: sub;
|
||||
}
|
||||
|
||||
._sup {
|
||||
font-size: smaller;
|
||||
vertical-align: super;
|
||||
}
|
||||
|
||||
._thead,
|
||||
._tbody,
|
||||
._tfoot {
|
||||
display: table-row-group;
|
||||
}
|
||||
|
||||
._tr {
|
||||
display: table-row;
|
||||
}
|
||||
|
||||
._td,
|
||||
._th {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
._th {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
._ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
._ul ._ul {
|
||||
margin: 0;
|
||||
list-style-type: circle;
|
||||
}
|
||||
|
||||
._ul ._ul ._ul {
|
||||
list-style-type: square;
|
||||
}
|
||||
|
||||
._abbr,
|
||||
._b,
|
||||
._code,
|
||||
._del,
|
||||
._em,
|
||||
._i,
|
||||
._ins,
|
||||
._label,
|
||||
._q,
|
||||
._span,
|
||||
._strong,
|
||||
._sub,
|
||||
._sup {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
/* #ifndef MP-ALIPAY || MP-TOUTIAO */
|
||||
._blockquote,
|
||||
._div,
|
||||
._p {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* #endif */
|
||||
1276
wechat-mini-program/components/mp-html/src/miniprogram/parser.js
Normal file
1276
wechat-mini-program/components/mp-html/src/miniprogram/parser.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,498 @@
|
||||
<template>
|
||||
<view id="_root" :class="(selectable?'_select ':'')+'_root'" :style="containerStyle">
|
||||
<slot v-if="!nodes[0]" />
|
||||
<!-- #ifndef APP-PLUS-NVUE -->
|
||||
<node v-else :childs="nodes" :opts="[lazyLoad,loadingImg,errorImg,showImgMenu,selectable]" name="span" />
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef APP-PLUS-NVUE -->
|
||||
<web-view ref="web" src="/static/app-plus/mp-html/local.html" :style="'margin-top:-2px;height:' + height + 'px'" @onPostMessage="_onMessage" />
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* mp-html v2.5.1
|
||||
* @description 富文本组件
|
||||
* @tutorial https://github.com/jin-yufeng/mp-html
|
||||
* @property {String} container-style 容器的样式
|
||||
* @property {String} content 用于渲染的 html 字符串
|
||||
* @property {Boolean} copy-link 是否允许外部链接被点击时自动复制
|
||||
* @property {String} domain 主域名,用于拼接链接
|
||||
* @property {String} error-img 图片出错时的占位图链接
|
||||
* @property {Boolean} lazy-load 是否开启图片懒加载
|
||||
* @property {string} loading-img 图片加载过程中的占位图链接
|
||||
* @property {Boolean} pause-video 是否在播放一个视频时自动暂停其他视频
|
||||
* @property {Boolean} preview-img 是否允许图片被点击时自动预览
|
||||
* @property {Boolean} scroll-table 是否给每个表格添加一个滚动层使其能单独横向滚动
|
||||
* @property {Boolean | String} selectable 是否开启长按复制
|
||||
* @property {Boolean} set-title 是否将 title 标签的内容设置到页面标题
|
||||
* @property {Boolean} show-img-menu 是否允许图片被长按时显示菜单
|
||||
* @property {Object} tag-style 标签的默认样式
|
||||
* @property {Boolean | Number} use-anchor 是否使用锚点链接
|
||||
* @event {Function} load dom 结构加载完毕时触发
|
||||
* @event {Function} ready 所有图片加载完毕时触发
|
||||
* @event {Function} imgtap 图片被点击时触发
|
||||
* @event {Function} linktap 链接被点击时触发
|
||||
* @event {Function} play 音视频播放时触发
|
||||
* @event {Function} error 媒体加载出错时触发
|
||||
*/
|
||||
// #ifndef APP-PLUS-NVUE
|
||||
import node from './node/node'
|
||||
// #endif
|
||||
import Parser from './parser'
|
||||
const plugins = []
|
||||
// #ifdef APP-PLUS-NVUE
|
||||
const dom = weex.requireModule('dom')
|
||||
// #endif
|
||||
export default {
|
||||
name: 'mp-html',
|
||||
data () {
|
||||
return {
|
||||
nodes: [],
|
||||
// #ifdef APP-PLUS-NVUE
|
||||
height: 3
|
||||
// #endif
|
||||
}
|
||||
},
|
||||
props: {
|
||||
containerStyle: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
content: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
copyLink: {
|
||||
type: [Boolean, String],
|
||||
default: true
|
||||
},
|
||||
domain: String,
|
||||
errorImg: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
lazyLoad: {
|
||||
type: [Boolean, String],
|
||||
default: false
|
||||
},
|
||||
loadingImg: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
pauseVideo: {
|
||||
type: [Boolean, String],
|
||||
default: true
|
||||
},
|
||||
previewImg: {
|
||||
type: [Boolean, String],
|
||||
default: true
|
||||
},
|
||||
scrollTable: [Boolean, String],
|
||||
selectable: [Boolean, String],
|
||||
setTitle: {
|
||||
type: [Boolean, String],
|
||||
default: true
|
||||
},
|
||||
showImgMenu: {
|
||||
type: [Boolean, String],
|
||||
default: true
|
||||
},
|
||||
tagStyle: Object,
|
||||
useAnchor: [Boolean, Number]
|
||||
},
|
||||
// #ifdef VUE3
|
||||
emits: ['load', 'ready', 'imgtap', 'linktap', 'play', 'error'],
|
||||
// #endif
|
||||
// #ifndef APP-PLUS-NVUE
|
||||
components: {
|
||||
node
|
||||
},
|
||||
// #endif
|
||||
watch: {
|
||||
content (content) {
|
||||
this.setContent(content)
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.plugins = []
|
||||
for (let i = plugins.length; i--;) {
|
||||
this.plugins.push(new plugins[i](this))
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
if (this.content && !this.nodes.length) {
|
||||
this.setContent(this.content)
|
||||
}
|
||||
},
|
||||
beforeDestroy () {
|
||||
this._hook('onDetached')
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* @description 将锚点跳转的范围限定在一个 scroll-view 内
|
||||
* @param {Object} page scroll-view 所在页面的示例
|
||||
* @param {String} selector scroll-view 的选择器
|
||||
* @param {String} scrollTop scroll-view scroll-top 属性绑定的变量名
|
||||
*/
|
||||
in (page, selector, scrollTop) {
|
||||
// #ifndef APP-PLUS-NVUE
|
||||
if (page && selector && scrollTop) {
|
||||
this._in = {
|
||||
page,
|
||||
selector,
|
||||
scrollTop
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 锚点跳转
|
||||
* @param {String} id 要跳转的锚点 id
|
||||
* @param {Number} offset 跳转位置的偏移量
|
||||
* @returns {Promise}
|
||||
*/
|
||||
navigateTo (id, offset) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.useAnchor) {
|
||||
reject(Error('Anchor is disabled'))
|
||||
return
|
||||
}
|
||||
offset = offset || parseInt(this.useAnchor) || 0
|
||||
// #ifdef APP-PLUS-NVUE
|
||||
if (!id) {
|
||||
dom.scrollToElement(this.$refs.web, {
|
||||
offset
|
||||
})
|
||||
resolve()
|
||||
} else {
|
||||
this._navigateTo = {
|
||||
resolve,
|
||||
reject,
|
||||
offset
|
||||
}
|
||||
this.$refs.web.evalJs('uni.postMessage({data:{action:"getOffset",offset:(document.getElementById(' + id + ')||{}).offsetTop}})')
|
||||
}
|
||||
// #endif
|
||||
// #ifndef APP-PLUS-NVUE
|
||||
let deep = ' '
|
||||
// #ifdef MP-WEIXIN || MP-QQ || MP-TOUTIAO
|
||||
deep = '>>>'
|
||||
// #endif
|
||||
const selector = uni.createSelectorQuery()
|
||||
// #ifndef MP-ALIPAY
|
||||
.in(this._in ? this._in.page : this)
|
||||
// #endif
|
||||
.select((this._in ? this._in.selector : '._root') + (id ? `${deep}#${id}` : '')).boundingClientRect()
|
||||
if (this._in) {
|
||||
selector.select(this._in.selector).scrollOffset()
|
||||
.select(this._in.selector).boundingClientRect()
|
||||
} else {
|
||||
// 获取 scroll-view 的位置和滚动距离
|
||||
selector.selectViewport().scrollOffset() // 获取窗口的滚动距离
|
||||
}
|
||||
selector.exec(res => {
|
||||
if (!res[0]) {
|
||||
reject(Error('Label not found'))
|
||||
return
|
||||
}
|
||||
const scrollTop = res[1].scrollTop + res[0].top - (res[2] ? res[2].top : 0) + offset
|
||||
if (this._in) {
|
||||
// scroll-view 跳转
|
||||
this._in.page[this._in.scrollTop] = scrollTop
|
||||
} else {
|
||||
// 页面跳转
|
||||
uni.pageScrollTo({
|
||||
scrollTop,
|
||||
duration: 300
|
||||
})
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
// #endif
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 获取文本内容
|
||||
* @return {String}
|
||||
*/
|
||||
getText (nodes) {
|
||||
let text = '';
|
||||
(function traversal (nodes) {
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i]
|
||||
if (node.type === 'text') {
|
||||
text += node.text.replace(/&/g, '&')
|
||||
} else if (node.name === 'br') {
|
||||
text += '\n'
|
||||
} else {
|
||||
// 块级标签前后加换行
|
||||
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')
|
||||
if (isBlock && text && text[text.length - 1] !== '\n') {
|
||||
text += '\n'
|
||||
}
|
||||
// 递归获取子节点的文本
|
||||
if (node.children) {
|
||||
traversal(node.children)
|
||||
}
|
||||
if (isBlock && text[text.length - 1] !== '\n') {
|
||||
text += '\n'
|
||||
} else if (node.name === 'td' || node.name === 'th') {
|
||||
text += '\t'
|
||||
}
|
||||
}
|
||||
}
|
||||
})(nodes || this.nodes)
|
||||
return text
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 获取内容大小和位置
|
||||
* @return {Promise}
|
||||
*/
|
||||
getRect () {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.createSelectorQuery()
|
||||
// #ifndef MP-ALIPAY
|
||||
.in(this)
|
||||
// #endif
|
||||
.select('#_root').boundingClientRect().exec(res => res[0] ? resolve(res[0]) : reject(Error('Root label not found')))
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 暂停播放媒体
|
||||
*/
|
||||
pauseMedia () {
|
||||
for (let i = (this._videos || []).length; i--;) {
|
||||
this._videos[i].pause()
|
||||
}
|
||||
// #ifdef APP-PLUS
|
||||
const command = 'for(var e=document.getElementsByTagName("video"),i=e.length;i--;)e[i].pause()'
|
||||
// #ifndef APP-PLUS-NVUE
|
||||
let page = this.$parent
|
||||
while (!page.$scope) page = page.$parent
|
||||
page.$scope.$getAppWebview().evalJS(command)
|
||||
// #endif
|
||||
// #ifdef APP-PLUS-NVUE
|
||||
this.$refs.web.evalJs(command)
|
||||
// #endif
|
||||
// #endif
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 设置媒体播放速率
|
||||
* @param {Number} rate 播放速率
|
||||
*/
|
||||
setPlaybackRate (rate) {
|
||||
this.playbackRate = rate
|
||||
for (let i = (this._videos || []).length; i--;) {
|
||||
this._videos[i].playbackRate(rate)
|
||||
}
|
||||
// #ifdef APP-PLUS
|
||||
const command = 'for(var e=document.getElementsByTagName("video"),i=e.length;i--;)e[i].playbackRate=' + rate
|
||||
// #ifndef APP-PLUS-NVUE
|
||||
let page = this.$parent
|
||||
while (!page.$scope) page = page.$parent
|
||||
page.$scope.$getAppWebview().evalJS(command)
|
||||
// #endif
|
||||
// #ifdef APP-PLUS-NVUE
|
||||
this.$refs.web.evalJs(command)
|
||||
// #endif
|
||||
// #endif
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 设置内容
|
||||
* @param {String} content html 内容
|
||||
* @param {Boolean} append 是否在尾部追加
|
||||
*/
|
||||
setContent (content, append) {
|
||||
if (!append || !this.imgList) {
|
||||
this.imgList = []
|
||||
}
|
||||
const nodes = new Parser(this).parse(content)
|
||||
// #ifdef APP-PLUS-NVUE
|
||||
if (this._ready) {
|
||||
this._set(nodes, append)
|
||||
}
|
||||
// #endif
|
||||
this.$set(this, 'nodes', append ? (this.nodes || []).concat(nodes) : nodes)
|
||||
|
||||
// #ifndef APP-PLUS-NVUE
|
||||
this._videos = []
|
||||
this.$nextTick(() => {
|
||||
this._hook('onLoad')
|
||||
this.$emit('load')
|
||||
})
|
||||
|
||||
if (this.lazyLoad || this.imgList._unloadimgs < this.imgList.length / 2) {
|
||||
// 设置懒加载,每 350ms 获取高度,不变则认为加载完毕
|
||||
let height = 0
|
||||
const callback = rect => {
|
||||
if (!rect || !rect.height) rect = {}
|
||||
// 350ms 总高度无变化就触发 ready 事件
|
||||
if (rect.height === height) {
|
||||
this.$emit('ready', rect)
|
||||
} else {
|
||||
height = rect.height
|
||||
setTimeout(() => {
|
||||
this.getRect().then(callback).catch(callback)
|
||||
}, 350)
|
||||
}
|
||||
}
|
||||
this.getRect().then(callback).catch(callback)
|
||||
} else {
|
||||
// 未设置懒加载,等待所有图片加载完毕
|
||||
if (!this.imgList._unloadimgs) {
|
||||
this.getRect().then(rect => {
|
||||
this.$emit('ready', rect)
|
||||
}).catch(() => {
|
||||
this.$emit('ready', {})
|
||||
})
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 调用插件钩子函数
|
||||
*/
|
||||
_hook (name) {
|
||||
for (let i = plugins.length; i--;) {
|
||||
if (this.plugins[i][name]) {
|
||||
this.plugins[i][name]()
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// #ifdef APP-PLUS-NVUE
|
||||
/**
|
||||
* @description 设置内容
|
||||
*/
|
||||
_set (nodes, append) {
|
||||
this.$refs.web.evalJs('setContent(' + JSON.stringify(nodes).replace(/%22/g, '') + ',' + JSON.stringify([this.containerStyle.replace(/(?:margin|padding)[^;]+/g, ''), this.errorImg, this.loadingImg, this.pauseVideo, this.scrollTable, this.selectable]) + ',' + append + ')')
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 接收到 web-view 消息
|
||||
*/
|
||||
_onMessage (e) {
|
||||
const message = e.detail.data[0]
|
||||
switch (message.action) {
|
||||
// web-view 初始化完毕
|
||||
case 'onJSBridgeReady':
|
||||
this._ready = true
|
||||
if (this.nodes) {
|
||||
this._set(this.nodes)
|
||||
}
|
||||
break
|
||||
// 内容 dom 加载完毕
|
||||
case 'onLoad':
|
||||
this.height = message.height
|
||||
this._hook('onLoad')
|
||||
this.$emit('load')
|
||||
break
|
||||
// 所有图片加载完毕
|
||||
case 'onReady':
|
||||
this.getRect().then(res => {
|
||||
this.$emit('ready', res)
|
||||
}).catch(() => {
|
||||
this.$emit('ready', {})
|
||||
})
|
||||
break
|
||||
// 总高度发生变化
|
||||
case 'onHeightChange':
|
||||
this.height = message.height
|
||||
break
|
||||
// 图片点击
|
||||
case 'onImgTap':
|
||||
this.$emit('imgtap', message.attrs)
|
||||
if (this.previewImg) {
|
||||
uni.previewImage({
|
||||
current: parseInt(message.attrs.i),
|
||||
urls: this.imgList
|
||||
})
|
||||
}
|
||||
break
|
||||
// 链接点击
|
||||
case 'onLinkTap': {
|
||||
const href = message.attrs.href
|
||||
this.$emit('linktap', message.attrs)
|
||||
if (href) {
|
||||
// 锚点跳转
|
||||
if (href[0] === '#') {
|
||||
if (this.useAnchor) {
|
||||
dom.scrollToElement(this.$refs.web, {
|
||||
offset: message.offset
|
||||
})
|
||||
}
|
||||
} else if (href.includes('://')) {
|
||||
// 打开外链
|
||||
if (this.copyLink) {
|
||||
plus.runtime.openWeb(href)
|
||||
}
|
||||
} else {
|
||||
uni.navigateTo({
|
||||
url: href,
|
||||
fail () {
|
||||
uni.switchTab({
|
||||
url: href
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'onPlay':
|
||||
this.$emit('play')
|
||||
break
|
||||
// 获取到锚点的偏移量
|
||||
case 'getOffset':
|
||||
if (typeof message.offset === 'number') {
|
||||
dom.scrollToElement(this.$refs.web, {
|
||||
offset: message.offset + this._navigateTo.offset
|
||||
})
|
||||
this._navigateTo.resolve()
|
||||
} else {
|
||||
this._navigateTo.reject(Error('Label not found'))
|
||||
}
|
||||
break
|
||||
// 点击
|
||||
case 'onClick':
|
||||
this.$emit('tap')
|
||||
this.$emit('click')
|
||||
break
|
||||
// 出错
|
||||
case 'onError':
|
||||
this.$emit('error', {
|
||||
source: message.source,
|
||||
attrs: message.attrs
|
||||
})
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* #ifndef APP-PLUS-NVUE */
|
||||
/* 根节点样式 */
|
||||
._root {
|
||||
padding: 1px 0;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
/* 长按复制 */
|
||||
._select {
|
||||
user-select: text;
|
||||
}
|
||||
/* #endif */
|
||||
</style>
|
||||
@@ -0,0 +1,595 @@
|
||||
<template>
|
||||
<view :id="attrs.id" :class="'_block _'+name+' '+attrs.class" :style="attrs.style">
|
||||
<block v-for="(n, i) in childs" v-bind:key="i">
|
||||
<!-- 图片 -->
|
||||
<!-- 占位图 -->
|
||||
<image v-if="n.name==='img'&&!n.t&&((opts[1]&&!ctrl[i])||ctrl[i]<0)" class="_img" :style="n.attrs.style" :src="ctrl[i]<0?opts[2]:opts[1]" mode="widthFix" />
|
||||
<!-- 显示图片 -->
|
||||
<!-- #ifdef H5 || (APP-PLUS && VUE2) -->
|
||||
<img v-if="n.name==='img'" :id="n.attrs.id" :class="'_img '+n.attrs.class" :style="(ctrl[i]===-1?'display:none;':'')+n.attrs.style" :src="n.attrs.src||(ctrl.load?n.attrs['data-src']:'')" :data-i="i" @load="imgLoad" @error="mediaError" @tap.stop="imgTap" @longpress="imgLongTap" />
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef H5 || (APP-PLUS && VUE2) -->
|
||||
<!-- 表格中的图片,使用 rich-text 防止大小不正确 -->
|
||||
<rich-text v-if="n.name==='img'&&n.t" :style="'display:'+n.t" :nodes="[{attrs:{style:n.attrs.style||'',src:n.attrs.src},name:'img'}]" :data-i="i" @tap.stop="imgTap" />
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef APP-HARMONY -->
|
||||
<image v-else-if="n.name==='img'" :id="n.attrs.id" :class="'_img '+n.attrs.class" :style="(ctrl[i]===-1?'display:none;':'')+'width:'+ctrl[i]+'px;'+n.attrs.style" :src="n.attrs.src||(ctrl.load?n.attrs['data-src']:'')" :mode="!n.h?'widthFix':(!n.w?'heightFix':(n.m||'scaleToFill'))" :data-i="i" @load="imgLoad" @error="mediaError" @tap.stop="imgTap" @longpress="imgLongTap" />
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef H5 || APP-PLUS || MP-KUAISHOU -->
|
||||
<image v-else-if="n.name==='img'" :id="n.attrs.id" :class="'_img '+n.attrs.class" :style="(ctrl[i]===-1?'display:none;':'')+'width:'+(ctrl[i]||1)+'px;height:1px;'+n.attrs.style" :src="n.attrs.src" :mode="!n.h?'widthFix':(!n.w?'heightFix':(n.m||'scaleToFill'))" :lazy-load="opts[0]" :webp="n.webp" :show-menu-by-longpress="opts[3]&&!n.attrs.ignore" :image-menu-prevent="!opts[3]||n.attrs.ignore" :data-i="i" @load="imgLoad" @error="mediaError" @tap.stop="imgTap" @longpress="imgLongTap" />
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef MP-KUAISHOU -->
|
||||
<image v-else-if="n.name==='img'" :id="n.attrs.id" :class="'_img '+n.attrs.class" :style="(ctrl[i]===-1?'display:none;':'')+n.attrs.style" :src="n.attrs.src" :lazy-load="opts[0]" :data-i="i" @load="imgLoad" @error="mediaError" @tap.stop="imgTap"></image>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef APP-PLUS && VUE3 -->
|
||||
<image v-else-if="n.name==='img'" :id="n.attrs.id" :class="'_img '+n.attrs.class" :style="(ctrl[i]===-1?'display:none;':'')+'width:'+(ctrl[i]||1)+'px;'+n.attrs.style" :src="n.attrs.src||(ctrl.load?n.attrs['data-src']:'')" :mode="!n.h?'widthFix':(!n.w?'heightFix':(n.m||''))" :data-i="i" @load="imgLoad" @error="mediaError" @tap.stop="imgTap" @longpress="imgLongTap" />
|
||||
<!-- #endif -->
|
||||
<!-- 文本 -->
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<text v-else-if="n.text" :user-select="opts[4]=='force'&&isiOS" decode>{{n.text}}</text>
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef MP-WEIXIN || MP-BAIDU || MP-ALIPAY || MP-TOUTIAO -->
|
||||
<text v-else-if="n.text" decode>{{n.text}}</text>
|
||||
<!-- #endif -->
|
||||
<text v-else-if="n.name==='br'">\n</text>
|
||||
<!-- 链接 -->
|
||||
<view v-else-if="n.name==='a'" :id="n.attrs.id" :class="(n.attrs.href?'_a ':'')+n.attrs.class" hover-class="_hover" :style="'display:inline;'+n.attrs.style" :data-i="i" @tap.stop="linkTap">
|
||||
<node name="span" :childs="n.children" :opts="opts" style="display:inherit" />
|
||||
</view>
|
||||
<!-- 视频 -->
|
||||
<!-- #ifdef APP-PLUS -->
|
||||
<view v-else-if="n.html" :id="n.attrs.id" :class="'_video '+n.attrs.class" :style="n.attrs.style" v-html="n.html" :data-i="i" @vplay.stop="play" />
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef APP-PLUS -->
|
||||
<video v-else-if="n.name==='video'" :id="n.attrs.id" :class="n.attrs.class" :style="n.attrs.style" :autoplay="n.attrs.autoplay" :controls="n.attrs.controls" :loop="n.attrs.loop" :muted="n.attrs.muted" :object-fit="n.attrs['object-fit']" :poster="n.attrs.poster" :src="n.src[ctrl[i]||0]" :data-i="i" @play="play" @error="mediaError" />
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef H5 || APP-PLUS -->
|
||||
<iframe v-else-if="n.name==='iframe'" :style="n.attrs.style" :allowfullscreen="n.attrs.allowfullscreen" :frameborder="n.attrs.frameborder" :src="n.attrs.src" />
|
||||
<embed v-else-if="n.name==='embed'" :style="n.attrs.style" :src="n.attrs.src" />
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef MP-TOUTIAO || ((H5 || APP-PLUS) && VUE3) -->
|
||||
<!-- 音频 -->
|
||||
<audio v-else-if="n.name==='audio'" :id="n.attrs.id" :class="n.attrs.class" :style="n.attrs.style" :author="n.attrs.author" :controls="n.attrs.controls" :loop="n.attrs.loop" :name="n.attrs.name" :poster="n.attrs.poster" :src="n.src[ctrl[i]||0]" :data-i="i" @play="play" @error="mediaError" />
|
||||
<!-- #endif -->
|
||||
<view v-else-if="(n.name==='table'&&n.c)||n.name==='li'" :id="n.attrs.id" :class="'_'+n.name+' '+n.attrs.class" :style="n.attrs.style">
|
||||
<node v-if="n.name==='li'" :childs="n.children" :opts="opts" />
|
||||
<view v-else v-for="(tbody, x) in n.children" v-bind:key="x" :class="'_'+tbody.name+' '+tbody.attrs.class" :style="tbody.attrs.style">
|
||||
<node v-if="tbody.name==='td'||tbody.name==='th'" :childs="tbody.children" :opts="opts" />
|
||||
<block v-else v-for="(tr, y) in tbody.children" v-bind:key="y">
|
||||
<view v-if="tr.name==='td'||tr.name==='th'" :class="'_'+tr.name+' '+tr.attrs.class" :style="tr.attrs.style">
|
||||
<node :childs="tr.children" :opts="opts" />
|
||||
</view>
|
||||
<view v-else :class="'_'+tr.name+' '+tr.attrs.class" :style="tr.attrs.style">
|
||||
<view v-for="(td, z) in tr.children" v-bind:key="z" :class="'_'+td.name+' '+td.attrs.class" :style="td.attrs.style">
|
||||
<node :childs="td.children" :opts="opts" />
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
<!-- insert -->
|
||||
<!-- 富文本 -->
|
||||
<!-- #ifdef H5 || ((MP-WEIXIN || MP-QQ || APP-PLUS || MP-360) && VUE2) -->
|
||||
<rich-text v-else-if="!n.c&&!handler.isInline(n.name, n.attrs.style)" :id="n.attrs.id" :style="n.f" :user-select="opts[4]" :nodes="[n]" />
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef H5 || ((MP-WEIXIN || MP-QQ || APP-PLUS || MP-360) && VUE2) -->
|
||||
<rich-text v-else-if="!n.c" :id="n.attrs.id" :style="'display:inline;'+n.f" :preview="false" :selectable="opts[4]" :user-select="opts[4]" :nodes="[n]" />
|
||||
<!-- #endif -->
|
||||
<!-- 继续递归 -->
|
||||
<view v-else-if="n.c===2" :id="n.attrs.id" :class="'_block _'+n.name+' '+n.attrs.class" :style="n.f+';'+n.attrs.style">
|
||||
<node v-for="(n2, j) in n.children" v-bind:key="j" :style="n2.f" :name="n2.name" :attrs="n2.attrs" :childs="n2.children" :opts="opts" />
|
||||
</view>
|
||||
<node v-else :style="n.f" :name="n.name" :attrs="n.attrs" :childs="n.children" :opts="opts" />
|
||||
</block>
|
||||
</view>
|
||||
</template>
|
||||
<script module="handler" lang="wxs">
|
||||
// 行内标签列表
|
||||
var inlineTags = {
|
||||
abbr: true,
|
||||
b: true,
|
||||
big: true,
|
||||
code: true,
|
||||
del: true,
|
||||
em: true,
|
||||
i: true,
|
||||
ins: true,
|
||||
label: true,
|
||||
q: true,
|
||||
small: true,
|
||||
span: true,
|
||||
strong: true,
|
||||
sub: true,
|
||||
sup: true
|
||||
}
|
||||
/**
|
||||
* @description 判断是否为行内标签
|
||||
*/
|
||||
module.exports = {
|
||||
isInline: function (tagName, style) {
|
||||
return inlineTags[tagName] || (style || '').indexOf('display:inline') !== -1
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script>
|
||||
import node from './node'
|
||||
export default {
|
||||
name: 'node',
|
||||
options: {
|
||||
// #ifdef MP-WEIXIN
|
||||
virtualHost: true,
|
||||
// #endif
|
||||
// #ifdef MP-TOUTIAO
|
||||
addGlobalClass: false
|
||||
// #endif
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
ctrl: {},
|
||||
// #ifdef MP-WEIXIN
|
||||
isiOS: uni.getSystemInfoSync().system.includes('iOS')
|
||||
// #endif
|
||||
}
|
||||
},
|
||||
props: {
|
||||
name: String,
|
||||
attrs: {
|
||||
type: Object,
|
||||
default () {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
childs: Array,
|
||||
opts: Array
|
||||
},
|
||||
components: {
|
||||
// #ifndef ((H5 || APP-PLUS) && VUE3) || APP-HARMONY
|
||||
node
|
||||
// #endif
|
||||
},
|
||||
mounted () {
|
||||
this.$nextTick(() => {
|
||||
for (this.root = this.$parent; this.root.$options.name !== 'mp-html'; this.root = this.root.$parent);
|
||||
})
|
||||
// #ifdef H5 || APP-PLUS
|
||||
if (this.opts[0]) {
|
||||
let i
|
||||
for (i = this.childs.length; i--;) {
|
||||
if (this.childs[i].name === 'img') break
|
||||
}
|
||||
if (i !== -1) {
|
||||
this.observer = uni.createIntersectionObserver(this).relativeToViewport({
|
||||
top: 500,
|
||||
bottom: 500
|
||||
})
|
||||
this.observer.observe('._img', res => {
|
||||
if (res.intersectionRatio) {
|
||||
this.$set(this.ctrl, 'load', 1)
|
||||
this.observer.disconnect()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
},
|
||||
beforeDestroy () {
|
||||
// #ifdef H5 || APP-PLUS
|
||||
if (this.observer) {
|
||||
this.observer.disconnect()
|
||||
}
|
||||
// #endif
|
||||
},
|
||||
methods: {
|
||||
// #ifdef MP-WEIXIN
|
||||
toJSON () { return this },
|
||||
// #endif
|
||||
/**
|
||||
* @description 播放视频事件
|
||||
* @param {Event} e
|
||||
*/
|
||||
play (e) {
|
||||
const i = e.currentTarget.dataset.i
|
||||
const node = this.childs[i]
|
||||
this.root.$emit('play', {
|
||||
source: node.name,
|
||||
attrs: {
|
||||
...node.attrs,
|
||||
src: node.src[this.ctrl[i] || 0]
|
||||
}
|
||||
})
|
||||
// #ifndef APP-PLUS
|
||||
if (this.root.pauseVideo) {
|
||||
let flag = false
|
||||
const id = e.target.id
|
||||
for (let i = this.root._videos.length; i--;) {
|
||||
if (this.root._videos[i].id === id) {
|
||||
flag = true
|
||||
} else {
|
||||
this.root._videos[i].pause() // 自动暂停其他视频
|
||||
}
|
||||
}
|
||||
// 将自己加入列表
|
||||
if (!flag) {
|
||||
const ctx = uni.createVideoContext(id
|
||||
// #ifndef MP-BAIDU
|
||||
, this
|
||||
// #endif
|
||||
)
|
||||
ctx.id = id
|
||||
if (this.root.playbackRate) {
|
||||
ctx.playbackRate(this.root.playbackRate)
|
||||
}
|
||||
this.root._videos.push(ctx)
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 图片点击事件
|
||||
* @param {Event} e
|
||||
*/
|
||||
imgTap (e) {
|
||||
const node = this.childs[e.currentTarget.dataset.i]
|
||||
if (node.a) {
|
||||
this.linkTap(node.a)
|
||||
return
|
||||
}
|
||||
if (node.attrs.ignore) return
|
||||
// #ifdef H5 || APP-PLUS
|
||||
node.attrs.src = node.attrs.src || node.attrs['data-src']
|
||||
// #endif
|
||||
// #ifndef APP-HARMONY
|
||||
this.root.$emit('imgtap', node.attrs)
|
||||
// #endif
|
||||
// #ifdef APP-HARMONY
|
||||
this.root.$emit('imgtap', {
|
||||
...node.attrs
|
||||
})
|
||||
// #endif
|
||||
// 自动预览图片
|
||||
if (this.root.previewImg) {
|
||||
uni.previewImage({
|
||||
// #ifdef MP-WEIXIN
|
||||
showmenu: this.root.showImgMenu,
|
||||
// #endif
|
||||
// #ifdef MP-ALIPAY
|
||||
enablesavephoto: this.root.showImgMenu,
|
||||
enableShowPhotoDownload: this.root.showImgMenu,
|
||||
// #endif
|
||||
current: parseInt(node.attrs.i),
|
||||
urls: this.root.imgList
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 图片长按
|
||||
*/
|
||||
imgLongTap (e) {
|
||||
// #ifdef APP-PLUS
|
||||
const attrs = this.childs[e.currentTarget.dataset.i].attrs
|
||||
if (this.opts[3] && !attrs.ignore) {
|
||||
uni.showActionSheet({
|
||||
itemList: ['保存图片'],
|
||||
success: () => {
|
||||
const save = path => {
|
||||
uni.saveImageToPhotosAlbum({
|
||||
filePath: path,
|
||||
success () {
|
||||
uni.showToast({
|
||||
title: '保存成功'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
if (this.root.imgList[attrs.i].startsWith('http')) {
|
||||
uni.downloadFile({
|
||||
url: this.root.imgList[attrs.i],
|
||||
success: res => save(res.tempFilePath)
|
||||
})
|
||||
} else {
|
||||
save(this.root.imgList[attrs.i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
// #endif
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 图片加载完成事件
|
||||
* @param {Event} e
|
||||
*/
|
||||
imgLoad (e) {
|
||||
const i = e.currentTarget.dataset.i
|
||||
/* #ifndef H5 || (APP-PLUS && VUE2) */
|
||||
if (!this.childs[i].w) {
|
||||
// 设置原宽度
|
||||
this.$set(this.ctrl, i, e.detail.width)
|
||||
} else /* #endif */ if ((this.opts[1] && !this.ctrl[i]) || this.ctrl[i] === -1) {
|
||||
// 加载完毕,取消加载中占位图
|
||||
this.$set(this.ctrl, i, 1)
|
||||
}
|
||||
this.checkReady()
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 检查是否所有图片加载完毕
|
||||
*/
|
||||
checkReady () {
|
||||
if (this.root && !this.root.lazyLoad) {
|
||||
this.root._unloadimgs -= 1
|
||||
if (!this.root._unloadimgs) {
|
||||
setTimeout(() => {
|
||||
this.root.getRect().then(rect => {
|
||||
this.root.$emit('ready', rect)
|
||||
}).catch(() => {
|
||||
this.root.$emit('ready', {})
|
||||
})
|
||||
}, 350)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 链接点击事件
|
||||
* @param {Event} e
|
||||
*/
|
||||
linkTap (e) {
|
||||
const node = e.currentTarget ? this.childs[e.currentTarget.dataset.i] : {}
|
||||
const attrs = node.attrs || e
|
||||
const href = attrs.href
|
||||
this.root.$emit('linktap', Object.assign({
|
||||
innerText: this.root.getText(node.children || []) // 链接内的文本内容
|
||||
}, attrs))
|
||||
if (href) {
|
||||
if (href[0] === '#') {
|
||||
// 跳转锚点
|
||||
this.root.navigateTo(href.substring(1)).catch(() => { })
|
||||
} else if (href.split('?')[0].includes('://')) {
|
||||
// 复制外部链接
|
||||
if (this.root.copyLink) {
|
||||
// #ifdef H5
|
||||
window.open(href)
|
||||
// #endif
|
||||
// #ifdef MP
|
||||
uni.setClipboardData({
|
||||
data: href,
|
||||
success: () =>
|
||||
uni.showToast({
|
||||
title: '链接已复制'
|
||||
})
|
||||
})
|
||||
// #endif
|
||||
// #ifdef APP-PLUS
|
||||
plus.runtime.openWeb(href)
|
||||
// #endif
|
||||
}
|
||||
} else {
|
||||
// 跳转页面
|
||||
uni.navigateTo({
|
||||
url: href,
|
||||
fail () {
|
||||
uni.switchTab({
|
||||
url: href,
|
||||
fail () { }
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 错误事件
|
||||
* @param {Event} e
|
||||
*/
|
||||
mediaError (e) {
|
||||
const i = e.currentTarget.dataset.i
|
||||
const node = this.childs[i]
|
||||
// 加载其他源
|
||||
if (node.name === 'video' || node.name === 'audio') {
|
||||
let index = (this.ctrl[i] || 0) + 1
|
||||
if (index > node.src.length) {
|
||||
index = 0
|
||||
}
|
||||
if (index < node.src.length) {
|
||||
this.$set(this.ctrl, i, index)
|
||||
return
|
||||
}
|
||||
} else if (node.name === 'img') {
|
||||
// #ifdef H5 && VUE3
|
||||
if (this.opts[0] && !this.ctrl.load) return
|
||||
// #endif
|
||||
// 显示错误占位图
|
||||
if (this.opts[2]) {
|
||||
this.$set(this.ctrl, i, -1)
|
||||
}
|
||||
this.checkReady()
|
||||
}
|
||||
if (this.root) {
|
||||
this.root.$emit('error', {
|
||||
source: node.name,
|
||||
attrs: node.attrs,
|
||||
// #ifndef H5 && VUE3
|
||||
errMsg: e.detail.errMsg
|
||||
// #endif
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
/* a 标签默认效果 */
|
||||
._a {
|
||||
padding: 1.5px 0 1.5px 0;
|
||||
color: #366092;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* a 标签点击态效果 */
|
||||
._hover {
|
||||
text-decoration: underline;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* 图片默认效果 */
|
||||
._img {
|
||||
max-width: 100%;
|
||||
-webkit-touch-callout: none;
|
||||
}
|
||||
|
||||
/* 内部样式 */
|
||||
|
||||
._block {
|
||||
display: block;
|
||||
}
|
||||
|
||||
._b,
|
||||
._strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
._code {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
._del {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
._em,
|
||||
._i {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
._h1 {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
._h2 {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
._h3 {
|
||||
font-size: 1.17em;
|
||||
}
|
||||
|
||||
._h5 {
|
||||
font-size: 0.83em;
|
||||
}
|
||||
|
||||
._h6 {
|
||||
font-size: 0.67em;
|
||||
}
|
||||
|
||||
._h1,
|
||||
._h2,
|
||||
._h3,
|
||||
._h4,
|
||||
._h5,
|
||||
._h6 {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
._image {
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
._ins {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
._li {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
._ol {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
|
||||
._ol,
|
||||
._ul {
|
||||
display: block;
|
||||
padding-left: 40px;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
._q::before {
|
||||
content: '"';
|
||||
}
|
||||
|
||||
._q::after {
|
||||
content: '"';
|
||||
}
|
||||
|
||||
._sub {
|
||||
font-size: smaller;
|
||||
vertical-align: sub;
|
||||
}
|
||||
|
||||
._sup {
|
||||
font-size: smaller;
|
||||
vertical-align: super;
|
||||
}
|
||||
|
||||
._thead,
|
||||
._tbody,
|
||||
._tfoot {
|
||||
display: table-row-group;
|
||||
}
|
||||
|
||||
._tr {
|
||||
display: table-row;
|
||||
}
|
||||
|
||||
._td,
|
||||
._th {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
._th {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
._ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
._ul ._ul {
|
||||
margin: 0;
|
||||
list-style-type: circle;
|
||||
}
|
||||
|
||||
._ul ._ul ._ul {
|
||||
list-style-type: square;
|
||||
}
|
||||
|
||||
._abbr,
|
||||
._b,
|
||||
._code,
|
||||
._del,
|
||||
._em,
|
||||
._i,
|
||||
._ins,
|
||||
._label,
|
||||
._q,
|
||||
._span,
|
||||
._strong,
|
||||
._sub,
|
||||
._sup {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
/* #ifdef APP-PLUS */
|
||||
._video {
|
||||
width: 300px;
|
||||
height: 225px;
|
||||
}
|
||||
/* #endif */
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,254 @@
|
||||
// 等待初始化完毕
|
||||
document.addEventListener('UniAppJSBridgeReady', () => {
|
||||
document.body.onclick = () =>
|
||||
uni.postMessage({
|
||||
data: {
|
||||
action: 'onClick'
|
||||
}
|
||||
})
|
||||
uni.postMessage({
|
||||
data: {
|
||||
action: 'onJSBridgeReady'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
let options
|
||||
let medias = []
|
||||
|
||||
/**
|
||||
* @description 获取标签的所有属性
|
||||
* @param {Element} ele
|
||||
*/
|
||||
function getAttrs (ele) {
|
||||
const attrs = Object.create(null)
|
||||
for (let i = ele.attributes.length; i--;) {
|
||||
attrs[ele.attributes[i].name] = ele.attributes[i].value
|
||||
}
|
||||
return attrs
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 图片加载出错
|
||||
*/
|
||||
function onImgError () {
|
||||
if (options[1]) {
|
||||
this.src = options[1]
|
||||
this.onerror = null
|
||||
}
|
||||
// 取消监听点击
|
||||
this.onclick = null
|
||||
this.ontouchstart = null
|
||||
uni.postMessage({
|
||||
data: {
|
||||
action: 'onError',
|
||||
source: 'img',
|
||||
attrs: getAttrs(this)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 检查是否所有图片加载完毕
|
||||
*/
|
||||
function checkReady () {
|
||||
window.unloadimgs -= 1
|
||||
if (window.unloadimgs === 0) {
|
||||
// 所有图片加载完毕
|
||||
uni.postMessage({
|
||||
data: {
|
||||
action: 'onReady'
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 创建 dom 结构
|
||||
* @param {object[]} nodes 节点数组
|
||||
* @param {Element} parent 父节点
|
||||
* @param {string} namespace 命名空间
|
||||
*/
|
||||
function createDom (nodes, parent, namespace) {
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i]
|
||||
let ele
|
||||
if (!node.type || node.type === 'node') {
|
||||
let name = node.name
|
||||
// svg 需要设置 namespace
|
||||
if (name === 'svg') {
|
||||
namespace = 'http://www.w3.org/2000/svg'
|
||||
}
|
||||
if (name === 'html' || name === 'body') {
|
||||
name = 'div'
|
||||
}
|
||||
// 创建标签
|
||||
if (!namespace) {
|
||||
ele = document.createElement(name)
|
||||
} else {
|
||||
ele = document.createElementNS(namespace, name)
|
||||
}
|
||||
// 设置属性
|
||||
for (const item in node.attrs) {
|
||||
ele.setAttribute(item, node.attrs[item])
|
||||
}
|
||||
// 递归创建子节点
|
||||
if (node.children) {
|
||||
createDom(node.children, ele, namespace)
|
||||
}
|
||||
|
||||
// 处理图片
|
||||
if (name === 'img') {
|
||||
window.unloadimgs += 1
|
||||
ele.onload = checkReady
|
||||
ele.onerror = checkReady
|
||||
if (!ele.src && ele.getAttribute('data-src')) {
|
||||
ele.src = ele.getAttribute('data-src')
|
||||
}
|
||||
if (!node.attrs.ignore) {
|
||||
// 监听图片点击事件
|
||||
ele.onclick = function (e) {
|
||||
e.stopPropagation()
|
||||
uni.postMessage({
|
||||
data: {
|
||||
action: 'onImgTap',
|
||||
attrs: getAttrs(this)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
if (options[2]) {
|
||||
const image = new Image()
|
||||
image.src = ele.src
|
||||
ele.src = options[2]
|
||||
image.onload = function () {
|
||||
ele.src = this.src
|
||||
}
|
||||
image.onerror = function () {
|
||||
ele.onerror()
|
||||
}
|
||||
}
|
||||
ele.onerror = onImgError
|
||||
} else if (name === 'a') {
|
||||
// 处理链接
|
||||
ele.addEventListener('click', function (e) {
|
||||
e.stopPropagation()
|
||||
e.preventDefault() // 阻止默认跳转
|
||||
const href = this.getAttribute('href')
|
||||
let offset
|
||||
if (href && href[0] === '#') {
|
||||
offset = (document.getElementById(href.substr(1)) || {}).offsetTop
|
||||
}
|
||||
uni.postMessage({
|
||||
data: {
|
||||
action: 'onLinkTap',
|
||||
attrs: getAttrs(this),
|
||||
offset
|
||||
}
|
||||
})
|
||||
}, true)
|
||||
} else if (name === 'video' || name === 'audio') {
|
||||
// 处理音视频
|
||||
medias.push(ele)
|
||||
if (!node.attrs.autoplay && !node.attrs.controls) {
|
||||
ele.setAttribute('controls', 'true')
|
||||
}
|
||||
ele.onplay = function () {
|
||||
uni.postMessage({
|
||||
data: {
|
||||
action: 'onPlay'
|
||||
}
|
||||
})
|
||||
if (options[3]) {
|
||||
for (let i = 0; i < medias.length; i++) {
|
||||
if (medias[i] !== this) {
|
||||
medias[i].pause()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ele.onerror = function () {
|
||||
uni.postMessage({
|
||||
data: {
|
||||
action: 'onError',
|
||||
source: name,
|
||||
attrs: getAttrs(this)
|
||||
}
|
||||
})
|
||||
}
|
||||
} else if (name === 'table' && options[4] && !ele.style.cssText.includes('inline')) {
|
||||
// 处理表格
|
||||
const div = document.createElement('div')
|
||||
div.style.overflow = 'auto'
|
||||
div.appendChild(ele)
|
||||
ele = div
|
||||
} else if (name === 'svg') {
|
||||
namespace = undefined
|
||||
}
|
||||
} else {
|
||||
ele = document.createTextNode(node.text.replace(/&/g, '&'))
|
||||
}
|
||||
parent.appendChild(ele)
|
||||
}
|
||||
}
|
||||
|
||||
// 设置 html 内容
|
||||
window.setContent = function (nodes, opts, append) {
|
||||
const ele = document.getElementById('content')
|
||||
|
||||
// 容器样式
|
||||
if (opts[0]) {
|
||||
document.body.style.cssText = opts[0]
|
||||
}
|
||||
|
||||
// 长按复制
|
||||
if (!opts[5]) {
|
||||
ele.style.userSelect = 'none'
|
||||
}
|
||||
|
||||
if (!append) {
|
||||
ele.innerHTML = '' // 不追加则先清空
|
||||
medias = []
|
||||
}
|
||||
|
||||
options = opts
|
||||
window.unloadimgs = 0
|
||||
const fragment = document.createDocumentFragment()
|
||||
createDom(nodes, fragment)
|
||||
ele.appendChild(fragment)
|
||||
|
||||
// 触发事件
|
||||
let height = ele.scrollHeight
|
||||
uni.postMessage({
|
||||
data: {
|
||||
action: 'onLoad',
|
||||
height
|
||||
}
|
||||
})
|
||||
if (!window.unloadimgs) {
|
||||
uni.postMessage({
|
||||
data: {
|
||||
action: 'onReady',
|
||||
height
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
clearInterval(window.timer)
|
||||
window.timer = setInterval(() => {
|
||||
if (ele.scrollHeight !== height) {
|
||||
height = ele.scrollHeight
|
||||
uni.postMessage({
|
||||
data: {
|
||||
action: 'onHeightChange',
|
||||
height: height
|
||||
}
|
||||
})
|
||||
}
|
||||
}, 350)
|
||||
}
|
||||
|
||||
// 回收计时器
|
||||
window.onunload = function () {
|
||||
clearInterval(window.timer)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
!function(e,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):(e=e||self).uni=n()}(this,(function(){"use strict";try{var e={};Object.defineProperty(e,"passive",{get:function(){!0}}),window.addEventListener("test-passive",null,e)}catch(e){}var n=Object.prototype.hasOwnProperty;function t(e,t){return n.call(e,t)}var i=[],a=function(e,n){var t={options:{timestamp:+new Date},name:e,arg:n};if(window.__dcloud_weex_postMessage||window.__dcloud_weex_){if("postMessage"===e){var a={data:[n]};return window.__dcloud_weex_postMessage?window.__dcloud_weex_postMessage(a):window.__dcloud_weex_.postMessage(JSON.stringify(a))}var o={type:"WEB_INVOKE_APPSERVICE",args:{data:t,webviewIds:i}};window.__dcloud_weex_postMessage?window.__dcloud_weex_postMessageToService(o):window.__dcloud_weex_.postMessageToService(JSON.stringify(o))}if(!window.plus)return window.parent.postMessage({type:"WEB_INVOKE_APPSERVICE",data:t,pageId:""},"*");if(0===i.length){var r=plus.webview.currentWebview();if(!r)throw new Error("plus.webview.currentWebview() is undefined");var d=r.parent(),s="";s=d?d.id:r.id,i.push(s)}if(plus.webview.getWebviewById("__uniapp__service"))plus.webview.postMessageToUniNView({type:"WEB_INVOKE_APPSERVICE",args:{data:t,webviewIds:i}},"__uniapp__service");else{var w=JSON.stringify(t);plus.webview.getLaunchWebview().evalJS('UniPlusBridge.subscribeHandler("'.concat("WEB_INVOKE_APPSERVICE",'",').concat(w,",").concat(JSON.stringify(i),");"))}},o={navigateTo:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=e.url;a("navigateTo",{url:encodeURI(n)})},navigateBack:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=e.delta;a("navigateBack",{delta:parseInt(n)||1})},switchTab:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=e.url;a("switchTab",{url:encodeURI(n)})},reLaunch:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=e.url;a("reLaunch",{url:encodeURI(n)})},redirectTo:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=e.url;a("redirectTo",{url:encodeURI(n)})},getEnv:function(e){window.plus?e({plus:!0}):e({h5:!0})},postMessage:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};a("postMessage",e.data||{})}},r=/uni-app/i.test(navigator.userAgent),d=/Html5Plus/i.test(navigator.userAgent),s=/complete|loaded|interactive/;var w=window.my&&navigator.userAgent.indexOf("AlipayClient")>-1;var u=window.swan&&window.swan.webView&&/swan/i.test(navigator.userAgent);var c=window.qq&&window.qq.miniProgram&&/QQ/i.test(navigator.userAgent)&&/miniProgram/i.test(navigator.userAgent);var g=window.tt&&window.tt.miniProgram&&/toutiaomicroapp/i.test(navigator.userAgent);var v=window.wx&&window.wx.miniProgram&&/micromessenger/i.test(navigator.userAgent)&&/miniProgram/i.test(navigator.userAgent);var p=window.qa&&/quickapp/i.test(navigator.userAgent);for(var l,_=function(){window.UniAppJSBridge=!0,document.dispatchEvent(new CustomEvent("UniAppJSBridgeReady",{bubbles:!0,cancelable:!0}))},f=[function(e){if(r||d)return window.__dcloud_weex_postMessage||window.__dcloud_weex_?document.addEventListener("DOMContentLoaded",e):window.plus&&s.test(document.readyState)?setTimeout(e,0):document.addEventListener("plusready",e),o},function(e){if(v)return window.WeixinJSBridge&&window.WeixinJSBridge.invoke?setTimeout(e,0):document.addEventListener("WeixinJSBridgeReady",e),window.wx.miniProgram},function(e){if(c)return window.QQJSBridge&&window.QQJSBridge.invoke?setTimeout(e,0):document.addEventListener("QQJSBridgeReady",e),window.qq.miniProgram},function(e){if(w){document.addEventListener("DOMContentLoaded",e);var n=window.my;return{navigateTo:n.navigateTo,navigateBack:n.navigateBack,switchTab:n.switchTab,reLaunch:n.reLaunch,redirectTo:n.redirectTo,postMessage:n.postMessage,getEnv:n.getEnv}}},function(e){if(u)return document.addEventListener("DOMContentLoaded",e),window.swan.webView},function(e){if(g)return document.addEventListener("DOMContentLoaded",e),window.tt.miniProgram},function(e){if(p){window.QaJSBridge&&window.QaJSBridge.invoke?setTimeout(e,0):document.addEventListener("QaJSBridgeReady",e);var n=window.qa;return{navigateTo:n.navigateTo,navigateBack:n.navigateBack,switchTab:n.switchTab,reLaunch:n.reLaunch,redirectTo:n.redirectTo,postMessage:n.postMessage,getEnv:n.getEnv}}},function(e){return document.addEventListener("DOMContentLoaded",e),o}],m=0;m<f.length&&!(l=f[m](_));m++);l||(l={});var E="undefined"!=typeof uni?uni:{};if(!E.navigateTo)for(var b in l)t(l,b)&&(E[b]=l[b]);return E.webView=l,E}));
|
||||
@@ -0,0 +1,33 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no">
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-x: scroll;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
video {
|
||||
width: 300px;
|
||||
height: 225px;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
-webkit-touch-callout: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="content" style="overflow: hidden;"></div>
|
||||
<script type="text/javascript" src="./js/uni.webview.min.js"></script>
|
||||
<script type="text/javascript" src="./js/handler.js"></script>
|
||||
</body>
|
||||
Reference in New Issue
Block a user