20261008
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
// 以下项目可以删减或更换顺序,但不能添加或更改名字
|
||||
export default {
|
||||
// 普通标签的菜单项
|
||||
node: ['大小', '颜色', '斜体', '粗体', '下划线', '居中', '缩进', '上移', '下移', '删除'],
|
||||
// 可以设置的文字颜色,此项可以添加 css 颜色
|
||||
color: ['red', 'yellow', 'blue', 'green', 'gray', 'white', 'black'],
|
||||
// 图片的菜单项
|
||||
img: ['换图', '宽度', '超链接', '预览图', '禁用预览', '上移', '下移', '删除'],
|
||||
// 链接的菜单项
|
||||
link: ['更换链接', '上移', '下移', '删除'],
|
||||
// 音视频的菜单项
|
||||
media: ['封面', '循环', '自动播放', '上移', '下移', '删除'],
|
||||
// 卡片的菜单项
|
||||
card: ['上移', '下移', '删除']
|
||||
}
|
||||
@@ -0,0 +1,553 @@
|
||||
/**
|
||||
* @fileoverview editable 插件
|
||||
*/
|
||||
import config from './config'
|
||||
import Parser from '../parser'
|
||||
|
||||
function Editable (vm) {
|
||||
this.vm = vm
|
||||
this.editHistory = [] // 历史记录
|
||||
this.editI = -1 // 历史记录指针
|
||||
vm._mask = [] // 蒙版被点击时进行的操作
|
||||
|
||||
vm._setData = function (path, val) {
|
||||
const paths = path.split('.')
|
||||
let target = vm
|
||||
for (let i = 0; i < paths.length - 1; i++) {
|
||||
target = target[paths[i]]
|
||||
}
|
||||
vm.$set(target, paths.pop(), val)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 移动历史记录指针
|
||||
* @param {Number} num 移动距离
|
||||
*/
|
||||
const move = num => {
|
||||
setTimeout(() => {
|
||||
const item = this.editHistory[this.editI + num]
|
||||
if (item) {
|
||||
this.editI += num
|
||||
vm._setData(item.key, item.value)
|
||||
}
|
||||
}, 200)
|
||||
}
|
||||
vm.undo = () => move(-1) // 撤销
|
||||
vm.redo = () => move(1) // 重做
|
||||
|
||||
/**
|
||||
* @description 更新记录
|
||||
* @param {String} path 更新内容路径
|
||||
* @param {*} oldVal 旧值
|
||||
* @param {*} newVal 新值
|
||||
* @param {Boolean} set 是否更新到视图
|
||||
* @private
|
||||
*/
|
||||
vm._editVal = (path, oldVal, newVal, set) => {
|
||||
// 当前指针后的内容去除
|
||||
while (this.editI < this.editHistory.length - 1) {
|
||||
this.editHistory.pop()
|
||||
}
|
||||
|
||||
// 最多存储 30 条操作记录
|
||||
while (this.editHistory.length > 30) {
|
||||
this.editHistory.pop()
|
||||
this.editI--
|
||||
}
|
||||
|
||||
const last = this.editHistory[this.editHistory.length - 1]
|
||||
if (!last || last.key !== path) {
|
||||
if (last) {
|
||||
// 去掉上一次的新值
|
||||
this.editHistory.pop()
|
||||
this.editI--
|
||||
}
|
||||
// 存入这一次的旧值
|
||||
this.editHistory.push({
|
||||
key: path,
|
||||
value: oldVal
|
||||
})
|
||||
this.editI++
|
||||
}
|
||||
|
||||
// 存入本次的新值
|
||||
this.editHistory.push({
|
||||
key: path,
|
||||
value: newVal
|
||||
})
|
||||
this.editI++
|
||||
|
||||
// 更新到视图
|
||||
if (set) {
|
||||
vm._setData(path, newVal)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 获取菜单项
|
||||
* @private
|
||||
*/
|
||||
vm._getItem = function (node, up, down) {
|
||||
let items
|
||||
let i
|
||||
if (node === 'color') {
|
||||
return config.color
|
||||
}
|
||||
if (node.name === 'img') {
|
||||
items = config.img.slice(0)
|
||||
if (!vm.getSrc) {
|
||||
i = items.indexOf('换图')
|
||||
if (i !== -1) {
|
||||
items.splice(i, 1)
|
||||
}
|
||||
i = items.indexOf('超链接')
|
||||
if (i !== -1) {
|
||||
items.splice(i, 1)
|
||||
}
|
||||
i = items.indexOf('预览图')
|
||||
if (i !== -1) {
|
||||
items.splice(i, 1)
|
||||
}
|
||||
}
|
||||
i = items.indexOf('禁用预览')
|
||||
if (i !== -1 && node.attrs.ignore) {
|
||||
items[i] = '启用预览'
|
||||
}
|
||||
} else if (node.name === 'a') {
|
||||
items = config.link.slice(0)
|
||||
if (!vm.getSrc) {
|
||||
i = items.indexOf('更换链接')
|
||||
if (i !== -1) {
|
||||
items.splice(i, 1)
|
||||
}
|
||||
}
|
||||
} else if (node.name === 'video' || node.name === 'audio') {
|
||||
items = config.media.slice(0)
|
||||
i = items.indexOf('封面')
|
||||
if (!vm.getSrc && i !== -1) {
|
||||
items.splice(i, 1)
|
||||
}
|
||||
i = items.indexOf('循环')
|
||||
if (node.attrs.loop && i !== -1) {
|
||||
items[i] = '不循环'
|
||||
}
|
||||
i = items.indexOf('自动播放')
|
||||
if (node.attrs.autoplay && i !== -1) {
|
||||
items[i] = '不自动播放'
|
||||
}
|
||||
} else if (node.name === 'card') {
|
||||
items = config.card.slice(0)
|
||||
} else {
|
||||
items = config.node.slice(0)
|
||||
}
|
||||
if (!up) {
|
||||
i = items.indexOf('上移')
|
||||
if (i !== -1) {
|
||||
items.splice(i, 1)
|
||||
}
|
||||
}
|
||||
if (!down) {
|
||||
i = items.indexOf('下移')
|
||||
if (i !== -1) {
|
||||
items.splice(i, 1)
|
||||
}
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 显示 tooltip
|
||||
* @param {object} obj
|
||||
* @private
|
||||
*/
|
||||
vm._tooltip = function (obj) {
|
||||
vm.$set(vm, 'tooltip', {
|
||||
top: obj.top,
|
||||
items: obj.items
|
||||
})
|
||||
vm._tooltipcb = obj.success
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 显示滚动条
|
||||
* @param {object} obj
|
||||
* @private
|
||||
*/
|
||||
vm._slider = function (obj) {
|
||||
vm.$set(vm, 'slider', {
|
||||
min: obj.min,
|
||||
max: obj.max,
|
||||
value: obj.value,
|
||||
top: obj.top
|
||||
})
|
||||
vm._slideringcb = obj.changing
|
||||
vm._slidercb = obj.change
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 显示颜色选择
|
||||
* @param {object} obj
|
||||
* @private
|
||||
*/
|
||||
vm._color = function (obj) {
|
||||
vm.$set(vm, 'color', {
|
||||
items: obj.items,
|
||||
top: obj.top
|
||||
})
|
||||
vm._colorcb = obj.success
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 点击蒙版
|
||||
* @private
|
||||
*/
|
||||
vm._maskTap = function () {
|
||||
// 隐藏所有悬浮窗
|
||||
while (vm._mask.length) {
|
||||
(vm._mask.pop())()
|
||||
}
|
||||
if (vm.tooltip) {
|
||||
vm.$set(vm, 'tooltip', null)
|
||||
}
|
||||
if (vm.slider) {
|
||||
vm.$set(vm, 'slider', null)
|
||||
}
|
||||
if (vm.color) {
|
||||
vm.$set(vm, 'color', null)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 插入节点
|
||||
* @param {Object} node
|
||||
*/
|
||||
function insert (node) {
|
||||
if (vm._edit) {
|
||||
vm._edit.insert(node)
|
||||
} else {
|
||||
const nodes = vm.nodes.slice(0)
|
||||
nodes.push(node)
|
||||
vm._editVal('nodes', vm.nodes, nodes, true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 在光标处插入指定 html 内容
|
||||
* @param {String} html 内容
|
||||
*/
|
||||
vm.insertHtml = html => {
|
||||
this.inserting = true
|
||||
const arr = new Parser(vm).parse(html)
|
||||
this.inserting = undefined
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
insert(arr[i])
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 在光标处插入图片
|
||||
*/
|
||||
vm.insertImg = function () {
|
||||
vm.getSrc && vm.getSrc('img').then(src => {
|
||||
if (typeof src === 'string') {
|
||||
src = [src]
|
||||
}
|
||||
const parser = new Parser(vm)
|
||||
for (let i = 0; i < src.length; i++) {
|
||||
insert({
|
||||
name: 'img',
|
||||
attrs: {
|
||||
src: parser.getUrl(src[i])
|
||||
}
|
||||
})
|
||||
}
|
||||
}).catch(() => { })
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 在光标处插入一个链接
|
||||
*/
|
||||
vm.insertLink = function () {
|
||||
vm.getSrc && vm.getSrc('link').then(url => {
|
||||
insert({
|
||||
name: 'a',
|
||||
attrs: {
|
||||
href: url
|
||||
},
|
||||
children: [{
|
||||
type: 'text',
|
||||
text: url
|
||||
}]
|
||||
})
|
||||
}).catch(() => { })
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 在光标处插入一个表格
|
||||
* @param {Number} rows 行数
|
||||
* @param {Number} cols 列数
|
||||
*/
|
||||
vm.insertTable = function (rows, cols) {
|
||||
const table = {
|
||||
name: 'table',
|
||||
attrs: {
|
||||
style: 'display:table;width:100%;margin:10px 0;text-align:center;border-spacing:0;border-collapse:collapse;border:1px solid gray'
|
||||
},
|
||||
children: []
|
||||
}
|
||||
for (let i = 0; i < rows; i++) {
|
||||
const tr = {
|
||||
name: 'tr',
|
||||
attrs: {},
|
||||
children: []
|
||||
}
|
||||
for (let j = 0; j < cols; j++) {
|
||||
tr.children.push({
|
||||
name: 'td',
|
||||
attrs: {
|
||||
style: 'padding:2px;border:1px solid gray'
|
||||
},
|
||||
children: [{
|
||||
type: 'text',
|
||||
text: ''
|
||||
}]
|
||||
})
|
||||
}
|
||||
table.children.push(tr)
|
||||
}
|
||||
insert(table)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 插入视频/音频
|
||||
* @param {Object} node
|
||||
*/
|
||||
function insertMedia (node) {
|
||||
if (typeof node.src === 'string') {
|
||||
node.src = [node.src]
|
||||
}
|
||||
const parser = new Parser(vm)
|
||||
// 拼接主域名
|
||||
for (let i = 0; i < node.src.length; i++) {
|
||||
node.src[i] = parser.getUrl(node.src[i])
|
||||
}
|
||||
insert({
|
||||
name: 'div',
|
||||
attrs: {
|
||||
style: 'text-align:center'
|
||||
},
|
||||
children: [node]
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 在光标处插入一个视频
|
||||
*/
|
||||
vm.insertVideo = function () {
|
||||
vm.getSrc && vm.getSrc('video').then(src => {
|
||||
insertMedia({
|
||||
name: 'video',
|
||||
attrs: {
|
||||
controls: 'T'
|
||||
},
|
||||
children: [],
|
||||
src,
|
||||
// #ifdef APP-PLUS
|
||||
html: `<video src="${src}" style="width:100%;height:100%"></video>`
|
||||
// #endif
|
||||
})
|
||||
}).catch(() => { })
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 在光标处插入一个音频
|
||||
*/
|
||||
vm.insertAudio = function () {
|
||||
vm.getSrc && vm.getSrc('audio').then(attrs => {
|
||||
let src
|
||||
if (attrs.src) {
|
||||
src = attrs.src
|
||||
attrs.src = undefined
|
||||
} else {
|
||||
src = attrs
|
||||
attrs = {}
|
||||
}
|
||||
attrs.controls = 'T'
|
||||
insertMedia({
|
||||
name: 'audio',
|
||||
attrs,
|
||||
children: [],
|
||||
src
|
||||
})
|
||||
}).catch(() => { })
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 在光标处插入一段文本
|
||||
*/
|
||||
vm.insertText = function () {
|
||||
insert({
|
||||
name: 'p',
|
||||
attrs: {},
|
||||
children: [{
|
||||
type: 'text',
|
||||
text: ''
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 清空内容
|
||||
*/
|
||||
vm.clear = function () {
|
||||
vm._maskTap()
|
||||
vm._edit = undefined
|
||||
vm.$set(vm, 'nodes', [{
|
||||
name: 'p',
|
||||
attrs: {},
|
||||
children: [{
|
||||
type: 'text',
|
||||
text: ''
|
||||
}]
|
||||
}])
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 获取编辑后的 html
|
||||
*/
|
||||
vm.getContent = function () {
|
||||
let html = '';
|
||||
// 递归遍历获取
|
||||
(function traversal (nodes, table) {
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
let item = nodes[i]
|
||||
if (item.type === 'text') {
|
||||
html += item.text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\n/g, '<br>').replace(/\xa0/g, ' ') // 编码实体
|
||||
} else {
|
||||
if (item.name === 'img') {
|
||||
item.attrs.i = ''
|
||||
// 还原被转换的 svg
|
||||
if ((item.attrs.src || '').includes('data:image/svg+xml;utf8,')) {
|
||||
html += item.attrs.src.substr(24).replace(/%23/g, '#').replace('<svg', '<svg style="' + (item.attrs.style || '') + '"')
|
||||
continue
|
||||
}
|
||||
} else if (item.name === 'video' || item.name === 'audio') {
|
||||
// 还原 video 和 audio 的 source
|
||||
item = JSON.parse(JSON.stringify(item))
|
||||
if (item.src.length > 1) {
|
||||
item.children = []
|
||||
for (let j = 0; j < item.src.length; j++) {
|
||||
item.children.push({
|
||||
name: 'source',
|
||||
attrs: {
|
||||
src: item.src[j]
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
item.attrs.src = item.src[0]
|
||||
}
|
||||
} else if (item.name === 'div' && (item.attrs.style || '').includes('overflow:auto') && (item.children[0] || {}).name === 'table') {
|
||||
// 还原滚动层
|
||||
item = item.children[0]
|
||||
}
|
||||
// 还原 table
|
||||
if (item.name === 'table') {
|
||||
item = JSON.parse(JSON.stringify(item))
|
||||
table = item.attrs
|
||||
if ((item.attrs.style || '').includes('display:grid')) {
|
||||
item.attrs.style = item.attrs.style.split('display:grid')[0]
|
||||
const children = [{
|
||||
name: 'tr',
|
||||
attrs: {},
|
||||
children: []
|
||||
}]
|
||||
for (let j = 0; j < item.children.length; j++) {
|
||||
item.children[j].attrs.style = item.children[j].attrs.style.replace(/grid-[^;]+;*/g, '')
|
||||
if (item.children[j].r !== children.length) {
|
||||
children.push({
|
||||
name: 'tr',
|
||||
attrs: {},
|
||||
children: [item.children[j]]
|
||||
})
|
||||
} else {
|
||||
children[children.length - 1].children.push(item.children[j])
|
||||
}
|
||||
}
|
||||
item.children = children
|
||||
}
|
||||
}
|
||||
html += '<' + item.name
|
||||
for (const attr in item.attrs) {
|
||||
let val = item.attrs[attr]
|
||||
if (!val) continue
|
||||
if (val === 'T' || val === true) {
|
||||
// bool 型省略值
|
||||
html += ' ' + attr
|
||||
continue
|
||||
} else if (item.name[0] === 't' && attr === 'style' && table) {
|
||||
// 取消为了显示 table 添加的 style
|
||||
val = val.replace(/;*display:table[^;]*/, '')
|
||||
if (table.border) {
|
||||
val = val.replace(/border[^;]+;*/g, $ => $.includes('collapse') ? $ : '')
|
||||
}
|
||||
if (table.cellpadding) {
|
||||
val = val.replace(/padding[^;]+;*/g, '')
|
||||
}
|
||||
if (!val) continue
|
||||
}
|
||||
html += ' ' + attr + '="' + val.replace(/"/g, '"') + '"'
|
||||
}
|
||||
html += '>'
|
||||
if (item.children) {
|
||||
traversal(item.children, table)
|
||||
html += '</' + item.name + '>'
|
||||
}
|
||||
}
|
||||
}
|
||||
})(vm.nodes)
|
||||
|
||||
// 其他插件处理
|
||||
for (let i = vm.plugins.length; i--;) {
|
||||
if (vm.plugins[i].onGetContent) {
|
||||
html = vm.plugins[i].onGetContent(html) || html
|
||||
}
|
||||
}
|
||||
|
||||
return html
|
||||
}
|
||||
}
|
||||
|
||||
Editable.prototype.onUpdate = function (content, config) {
|
||||
if (this.vm.editable) {
|
||||
this.vm._maskTap()
|
||||
config.entities.amp = '&'
|
||||
if (!this.inserting) {
|
||||
this.vm._edit = undefined
|
||||
if (!content) {
|
||||
setTimeout(() => {
|
||||
this.vm.$set(this.vm, 'nodes', [{
|
||||
name: 'p',
|
||||
attrs: {},
|
||||
children: [{
|
||||
type: 'text',
|
||||
text: ''
|
||||
}]
|
||||
}])
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Editable.prototype.onParse = function (node) {
|
||||
// 空白单元格可编辑
|
||||
if (this.vm.editable && (node.name === 'td' || node.name === 'th') && !this.vm.getText(node.children)) {
|
||||
node.children.push({
|
||||
type: 'text',
|
||||
text: ''
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default Editable
|
||||
@@ -0,0 +1,596 @@
|
||||
<template>
|
||||
<view id="_root" :class="(selectable?'_select ':'')+'_root'" :style="(editable?'min-height:200px;':'')+containerStyle" @tap="_containTap">
|
||||
<slot v-if="!nodes[0]" />
|
||||
<!-- #ifndef APP-PLUS-NVUE -->
|
||||
<node v-else :childs="nodes" :opts="[lazyLoad,loadingImg,errorImg,showImgMenu,selectable,editable,placeholder,'nodes']" 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 v-if="tooltip" class="_tooltip_contain" :style="'top:'+tooltip.top+'px'">
|
||||
<view class="_tooltip">
|
||||
<view v-for="(item, index) in tooltip.items" v-bind:key="index" class="_tooltip_item" :data-i="index" @tap="_tooltipTap">{{item}}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="slider" class="_slider" :style="'top:'+slider.top+'px'">
|
||||
<slider :value="slider.value" :min="slider.min" :max="slider.max" handle-size="14" block-size="14" show-value activeColor="white" style="padding:3px" @changing="_sliderChanging" @change="_sliderChange" />
|
||||
</view>
|
||||
<view v-if="color" class="_tooltip_contain" :style="'top:'+color.top+'px'">
|
||||
<view class="_tooltip" style="overflow-y: hidden;">
|
||||
<view v-for="(item, index) in color.items" v-bind:key="index" class="_color_item" :style="'background-color:'+item" :data-i="index" @tap="_colorTap"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* mp-html v2.4.3
|
||||
* @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'
|
||||
import style from './style/index.js'
|
||||
import editable from './editable/index.js'
|
||||
const plugins=[style,editable,]
|
||||
// #ifdef APP-PLUS-NVUE
|
||||
const dom = weex.requireModule('dom')
|
||||
// #endif
|
||||
export default {
|
||||
name: 'mp-html',
|
||||
data() {
|
||||
return {
|
||||
tooltip: null,
|
||||
slider: null,
|
||||
color: null,
|
||||
nodes: [],
|
||||
// #ifdef APP-PLUS-NVUE
|
||||
height: 3
|
||||
// #endif
|
||||
}
|
||||
},
|
||||
props: {
|
||||
editable: Boolean,
|
||||
placeholder: String,
|
||||
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: {
|
||||
editable(val) {
|
||||
this.setContent(val ? this.content : this.getContent())
|
||||
if (!val)
|
||||
this._maskTap()
|
||||
},
|
||||
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.editable) && !this.nodes.length) {
|
||||
this.setContent(this.content)
|
||||
}
|
||||
},
|
||||
beforeDestroy () {
|
||||
this._hook('onDetached')
|
||||
},
|
||||
methods: {
|
||||
_containTap() {
|
||||
if (!this._lock && !this.slider && !this.color) {
|
||||
this._edit = undefined
|
||||
this._maskTap()
|
||||
}
|
||||
},
|
||||
_tooltipTap(e) {
|
||||
this._tooltipcb(e.currentTarget.dataset.i)
|
||||
this.$set(this, 'tooltip', null)
|
||||
},
|
||||
_sliderChanging(e) {
|
||||
this._slideringcb(e.detail.value)
|
||||
},
|
||||
_sliderChange(e) {
|
||||
this._slidercb(e.detail.value)
|
||||
},
|
||||
_colorTap(e) {
|
||||
this._colorcb(e.currentTarget.dataset.i)
|
||||
this.$set(this, 'color', null)
|
||||
},
|
||||
/**
|
||||
* @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 {
|
||||
vk.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 */
|
||||
|
||||
/* 提示条 */
|
||||
._tooltip_contain {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
left: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
._tooltip {
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
height: 30px;
|
||||
padding: 0 3px;
|
||||
overflow: scroll;
|
||||
font-size: 14px;
|
||||
line-height: 30px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
._tooltip_item {
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
padding: 0 2vw;
|
||||
line-height: 30px;
|
||||
background-color: black;
|
||||
color: white;
|
||||
}
|
||||
|
||||
._color_item {
|
||||
display: inline-block;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin: 5px 2vw;
|
||||
border:1px solid #dfe2e5;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* 图片宽度滚动条 */
|
||||
._slider {
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
._tooltip,
|
||||
._slider {
|
||||
background-color: black;
|
||||
border-radius: 3px;
|
||||
opacity: 0.75;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* @fileoverview style 插件
|
||||
*/
|
||||
// #ifndef APP-PLUS-NVUE
|
||||
import Parser from './parser'
|
||||
// #endif
|
||||
|
||||
function Style () {
|
||||
this.styles = []
|
||||
}
|
||||
|
||||
// #ifndef APP-PLUS-NVUE
|
||||
Style.prototype.onParse = function (node, vm) {
|
||||
// 获取样式
|
||||
if (node.name === 'style' && node.children.length && node.children[0].type === 'text') {
|
||||
this.styles = this.styles.concat(new Parser().parse(node.children[0].text))
|
||||
} else if (node.name) {
|
||||
// 匹配样式(对非文本标签)
|
||||
// 存储不同优先级的样式 name < class < id < 后代
|
||||
let matched = ['', '', '', '']
|
||||
for (let i = 0, len = this.styles.length; i < len; i++) {
|
||||
const item = this.styles[i]
|
||||
let res = match(node, item.key || item.list[item.list.length - 1])
|
||||
let j
|
||||
if (res) {
|
||||
// 后代选择器
|
||||
if (!item.key) {
|
||||
j = item.list.length - 2
|
||||
for (let k = vm.stack.length; j >= 0 && k--;) {
|
||||
// 子选择器
|
||||
if (item.list[j] === '>') {
|
||||
// 错误情况
|
||||
if (j < 1 || j > item.list.length - 2) break
|
||||
if (match(vm.stack[k], item.list[j - 1])) {
|
||||
j -= 2
|
||||
} else {
|
||||
j++
|
||||
}
|
||||
} else if (match(vm.stack[k], item.list[j])) {
|
||||
j--
|
||||
}
|
||||
}
|
||||
res = 4
|
||||
}
|
||||
if (item.key || j < 0) {
|
||||
// 添加伪类
|
||||
if (item.pseudo && node.children) {
|
||||
let text
|
||||
item.style = item.style.replace(/content:([^;]+)/, (_, $1) => {
|
||||
text = $1.replace(/['"]/g, '')
|
||||
// 处理 attr 函数
|
||||
.replace(/attr\((.+?)\)/, (_, $1) => node.attrs[$1.trim()] || '')
|
||||
// 编码 \xxx
|
||||
.replace(/\\(\w{4})/, (_, $1) => String.fromCharCode(parseInt($1, 16)))
|
||||
return ''
|
||||
})
|
||||
const pseudo = {
|
||||
name: 'span',
|
||||
attrs: {
|
||||
style: item.style
|
||||
},
|
||||
children: [{
|
||||
type: 'text',
|
||||
text
|
||||
}]
|
||||
}
|
||||
if (item.pseudo === 'before') {
|
||||
node.children.unshift(pseudo)
|
||||
} else {
|
||||
node.children.push(pseudo)
|
||||
}
|
||||
} else {
|
||||
matched[res - 1] += item.style + (item.style[item.style.length - 1] === ';' ? '' : ';')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
matched = matched.join('')
|
||||
if (matched.length > 2) {
|
||||
node.attrs.style = matched + (node.attrs.style || '')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 匹配样式
|
||||
* @param {object} node 要匹配的标签
|
||||
* @param {string|string[]} keys 选择器
|
||||
* @returns {number} 0:不匹配;1:name 匹配;2:class 匹配;3:id 匹配
|
||||
*/
|
||||
function match (node, keys) {
|
||||
function matchItem (key) {
|
||||
if (key[0] === '#') {
|
||||
// 匹配 id
|
||||
if (node.attrs.id && node.attrs.id.trim() === key.substr(1)) return 3
|
||||
} else if (key[0] === '.') {
|
||||
// 匹配 class
|
||||
key = key.substr(1)
|
||||
const selectors = (node.attrs.class || '').split(' ')
|
||||
for (let i = 0; i < selectors.length; i++) {
|
||||
if (selectors[i].trim() === key) return 2
|
||||
}
|
||||
} else if (node.name === key) {
|
||||
// 匹配 name
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// 多选择器交集
|
||||
if (keys instanceof Array) {
|
||||
let res = 0
|
||||
for (let j = 0; j < keys.length; j++) {
|
||||
const tmp = matchItem(keys[j])
|
||||
// 任意一个不匹配就失败
|
||||
if (!tmp) return 0
|
||||
// 优先级最大的一个作为最终优先级
|
||||
if (tmp > res) {
|
||||
res = tmp
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
return matchItem(keys)
|
||||
}
|
||||
// #endif
|
||||
|
||||
export default Style
|
||||
@@ -0,0 +1,175 @@
|
||||
const blank = {
|
||||
' ': true,
|
||||
'\n': true,
|
||||
'\t': true,
|
||||
'\r': true,
|
||||
'\f': true
|
||||
}
|
||||
|
||||
function Parser () {
|
||||
this.styles = []
|
||||
this.selectors = []
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 解析 css 字符串
|
||||
* @param {string} content css 内容
|
||||
*/
|
||||
Parser.prototype.parse = function (content) {
|
||||
new Lexer(this).parse(content)
|
||||
return this.styles
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 解析到一个选择器
|
||||
* @param {string} name 名称
|
||||
*/
|
||||
Parser.prototype.onSelector = function (name) {
|
||||
// 不支持的选择器
|
||||
if (name.includes('[') || name.includes('*') || name.includes('@')) return
|
||||
const selector = {}
|
||||
// 伪类
|
||||
if (name.includes(':')) {
|
||||
const info = name.split(':')
|
||||
const pseudo = info.pop()
|
||||
if (pseudo === 'before' || pseudo === 'after') {
|
||||
selector.pseudo = pseudo
|
||||
name = info[0]
|
||||
} else return
|
||||
}
|
||||
|
||||
// 分割交集选择器
|
||||
function splitItem (str) {
|
||||
const arr = []
|
||||
let i, start
|
||||
for (i = 1, start = 0; i < str.length; i++) {
|
||||
if (str[i] === '.' || str[i] === '#') {
|
||||
arr.push(str.substring(start, i))
|
||||
start = i
|
||||
}
|
||||
}
|
||||
if (!arr.length) {
|
||||
return str
|
||||
} else {
|
||||
arr.push(str.substring(start, i))
|
||||
return arr
|
||||
}
|
||||
}
|
||||
|
||||
// 后代选择器
|
||||
if (name.includes(' ')) {
|
||||
selector.list = []
|
||||
const list = name.split(' ')
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
if (list[i].length) {
|
||||
// 拆分子选择器
|
||||
const arr = list[i].split('>')
|
||||
for (let j = 0; j < arr.length; j++) {
|
||||
selector.list.push(splitItem(arr[j]))
|
||||
if (j < arr.length - 1) {
|
||||
selector.list.push('>')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
selector.key = splitItem(name)
|
||||
}
|
||||
|
||||
this.selectors.push(selector)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 解析到选择器内容
|
||||
* @param {string} content 内容
|
||||
*/
|
||||
Parser.prototype.onContent = function (content) {
|
||||
// 并集选择器
|
||||
for (let i = 0; i < this.selectors.length; i++) {
|
||||
this.selectors[i].style = content
|
||||
}
|
||||
this.styles = this.styles.concat(this.selectors)
|
||||
this.selectors = []
|
||||
}
|
||||
|
||||
/**
|
||||
* @description css 词法分析器
|
||||
* @param {object} handler 高层处理器
|
||||
*/
|
||||
function Lexer (handler) {
|
||||
this.selector = ''
|
||||
this.style = ''
|
||||
this.handler = handler
|
||||
}
|
||||
|
||||
Lexer.prototype.parse = function (content) {
|
||||
this.i = 0
|
||||
this.content = content
|
||||
this.state = this.blank
|
||||
for (let len = content.length; this.i < len; this.i++) {
|
||||
this.state(content[this.i])
|
||||
}
|
||||
}
|
||||
|
||||
Lexer.prototype.comment = function () {
|
||||
this.i = this.content.indexOf('*/', this.i) + 1
|
||||
if (!this.i) {
|
||||
this.i = this.content.length
|
||||
}
|
||||
}
|
||||
|
||||
Lexer.prototype.blank = function (c) {
|
||||
if (!blank[c]) {
|
||||
if (c === '/' && this.content[this.i + 1] === '*') {
|
||||
this.comment()
|
||||
return
|
||||
}
|
||||
this.selector += c
|
||||
this.state = this.name
|
||||
}
|
||||
}
|
||||
|
||||
Lexer.prototype.name = function (c) {
|
||||
if (c === '/' && this.content[this.i + 1] === '*') {
|
||||
this.comment()
|
||||
return
|
||||
}
|
||||
if (c === '{' || c === ',' || c === ';') {
|
||||
this.handler.onSelector(this.selector.trimEnd())
|
||||
this.selector = ''
|
||||
if (c !== '{') {
|
||||
while (blank[this.content[++this.i]]);
|
||||
}
|
||||
if (this.content[this.i] === '{') {
|
||||
this.floor = 1
|
||||
this.state = this.val
|
||||
} else {
|
||||
this.selector += this.content[this.i]
|
||||
}
|
||||
} else if (blank[c]) {
|
||||
this.selector += ' '
|
||||
} else {
|
||||
this.selector += c
|
||||
}
|
||||
}
|
||||
|
||||
Lexer.prototype.val = function (c) {
|
||||
if (c === '/' && this.content[this.i + 1] === '*') {
|
||||
this.comment()
|
||||
return
|
||||
}
|
||||
if (c === '{') {
|
||||
this.floor++
|
||||
} else if (c === '}') {
|
||||
this.floor--
|
||||
if (!this.floor) {
|
||||
this.handler.onContent(this.style)
|
||||
this.style = ''
|
||||
this.state = this.blank
|
||||
return
|
||||
}
|
||||
}
|
||||
this.style += c
|
||||
}
|
||||
|
||||
export default Parser
|
||||
Reference in New Issue
Block a user