评论

小程序富文本Editor插入图片、超链接、公式等的一次尝试

小程序富文本Editor插入图片、超链接、公式等的一次尝试

小程序插入图片

通过EditorContext.insertImage接口可以实现图片的插入:

EditorContext.insertImage({
	src,
    width,
    height,
    data,
})

如何插入超链接、公式、视频、表格等等?

通过EditorContext.insertCustomBlock应该是可以实现的,具体实现方式我没有了解过,不过我使用的Taro3框架的版本,不支持渲染editor-portal组件。

于是我通过EditorContext.insertImage接口实现了这些功能。

思路是什么?

  1. 首先,渲染层面:公式可以通过Mathjax库渲染成svg图片;超链接可以通过svg图片的形式渲染成文字图片(如果嫌手动生成svg麻烦,也可以通过Mathjax渲染超链接的文字);表格可以通过svg图片的形式渲染成表格图片(如果嫌手动生成svg麻烦,也可以通过Mathjax渲染表格);视频可以渲染一张封面图或者一张默认图片(点击后可以修改视频或者播放视频等)

  2. 然后,数据层面:上述元素都是图片,如何区分。可以通过EditorContext.insertImage中的data字段来保存type类型和相关属性数据,最终会渲染到html的data-custom中去。

  3. 其次,编辑层面:编辑器中删除按钮可以直接删除图片,然后如果实现点击元素,弹出修改窗口,就可以实现上述元素的修改操作(Editor需要关闭show-img-size、show-img-toolbar、show-img-resize这三个参数),所以需要一种方案实现点击能知道点击的是哪个元素。

  4. 最后,生成的HTML:最终生成的HTML字符串中,上述元素都是img,需要通过data-custom中的type字段来还原成对应的HTML字符串。

尝试实践:

插入元素

我以插入超链接为例:

    async insertLink() {
        const { edit_index, link_text, link_url } = this.state
        if (edit_index >= 0) {
            await this.props.getEditorContext().deleteText({
                index: edit_index,
                length: 1,
            })
            await this.props.getEditorContext().setSelection({
                index: edit_index,
                length: 0,
            })
        }

        let link_info = TexUtil.generateTexInfo(`\\underline{${link_text}}`)
        // console.log(link_info)

        this.props.getEditorContext().insertImage({
            extClass: link_info.extClass,
            src: link_info.src,
            nowrap: true,
            alt: link_text,
            data: {
                data: WXEditorHelper.encodeDataCustom({
                    type: 'link',
                    href: link_url,
                    text: link_text
                })
            },
            width: link_info.width,
            height: link_info.height,
        })
        console.log('link ext class:', link_info.extClass)

        this.closeLinkDialog()
    }
  1. extClass用于给图片加上class,因为不能设置style,目前只能通过这种方式给图片加样式。像超链接、公式图片,因为需要和文字对齐,需要设置类似于vertical-align: -0.1em这种(Mathjax生成的公式里有对应的属性);然后文字图片需要根据font-size来进行缩放,需要设置类似于width: 3emheight: 1.5em这种。
    因为不能加style,只能通过class实现,所以只能设置类似于class="width-30em height-15em verticalAlign--1em"这种,通过预先设置一堆固定的class,然后xxem放大10倍后进行四舍五入,比如width: 2.45em对应的class就是width-24em。通过这种方式能近似实现。
    我使用的Taro框架,可以通过less预先生成一堆这种类:
@maxWidth: 100;
@maxHeight: 100;
@minVerticalAlign: -100;
@maxVerticalAlign: 100;

// 下面width、height、verticalAlign均为公式图片所需样式
// 批量生成宽度样式
.generateWidth(@n) when (@n > 0) {
    .width-@{n}em {
        width: (@n / 10em);
    }
    .generateWidth(@n - 1);
}

.generateWidth(@maxWidth);

// 批量生成高度样式
.generateHeight(@n) when (@n > 0) {
    .height-@{n}em {
        height: (@n / 10em);
    }
    .generateHeight(@n - 1);
}

.generateHeight(@maxHeight);

// 批量生成对齐样式
.generateVerticalAlign(@n) when (@n > @minVerticalAlign) {
    .verticalAlign-@{n}em {
        vertical-align: (@n / 10em);
    }
    .generateVerticalAlign(@n - 1);
}

.generateVerticalAlign(@maxVerticalAlign);
  1. src的话,超链接、公式、表格等都是svg图片,通过base64处理后传给src字段:
src: `data:image/svg+xml;base64,${base64.encode(svg_str)}`
  1. data字段,用于存入元素的类型和属性能相关数据:
data: {
	data: WXEditorHelper.encodeDataCustom({
		type: 'link',
		href: link_url,
		text: link_text
	})
},

这里我自定义了一个WXEditorHelper.encodeDataCustom接口:

static encodeDataCustom(data) {
	return base64.encode(JSON.stringify(data))
}

处理成base64,防止最终生成的html中data-custom字段在出现转义、解析困难等问题。

svg的生成

svg图片字符串,公式的话可以通过Mathjax生成,超链接可以自己手动生成,或者使用也使用Mathjax。

不过小程序中使用Mathjax,好像直接使用有困难。我用的Taro框架,所以我找了一个react-native的Mathjax库,然后改了一下,用到了小程序中。由于Mathjax比较大,需要进行分包异步加载。

元素点击事件

小程序Editor没有直接提供点击某个元素,触发相关事件的功能。需要自己来实现。

我的实现思路是:给Editor外层加上点击事件,通过解析Editor数据data中的delta字段,遍历所有字符,通过EditorContext.getBounds函数来判断点击的坐标是否在该字符的坐标范围内(图片占一个字符)。因为点击事件中const { x, y } = e.detail的x和y是相对于屏幕左上角,EditorContext.getBounds得到的bounds也是相对于屏幕左上角,所以即使Editor内部有滚动也不影响。

下面是实现代码(对于delta字段解析不太确定是否准确):

    getWxDeltaLength(delta) {
        const { ops } = delta
        if (!ops) {
            return 0
        }
        let all_length = 0
        for (let i = 0; i < ops.length; i++) {
            let item = ops[i]
            if (!item.insert) {
                continue 
            }
            if (item.insert.image) {
                all_length += 1 // 图片算一个字符
            }
            else {
                all_length += item.insert.length
            }
        }

        return all_length
    }

    getWxDeltaIndexType(delta, index) {
        const { ops } = delta
        if (!ops) {
            return {
                type: 'text',
            }
        }
        let now_index = 0
        for (let i = 0; i < ops.length; i++) {
            let item = ops[i]
            if (!item.insert) {
                continue 
            }
            let old_index = now_index
            if (item.insert.image) {
                now_index += 1 // 图片算一个字符
            }
            else {
                now_index += item.insert.length
            }
            if (old_index <= index && index < now_index) {
                if (item.insert.image) {
                    let data_custom = WXEditorHelper.decodeDataCustom(item.attributes['data-custom'])
                    console.log(data_custom)
                    if (data_custom && data_custom.type == 'tex') {
                        return {
                            type: 'tex',
                            data: data_custom,
                        }
                    }
                    if (data_custom && data_custom.type == 'link') {
                        return {
                            type: 'link',
                            data: data_custom,
                        }
                    }
                    if (data_custom && data_custom.type == 'table') {
                        return {
                            type: 'table',
                            data: data_custom,
                        }
                    }
                    return {
                        type: 'image',
                        src: item.insert.image,
                        // width: item.attributes && item.attributes.width ? item.attributes.width : null,
                        data: item.attributes ? data_custom : null,
                    }
                }
                else {
                    return {
                        type: 'text'
                    }
                }
            }
        }

        return {
            type: 'text'
        }
    }
    
    async onClickEditor(e) {
        let touch_item = e.touches[0]
        let x = touch_item.clientX
        let y = touch_item.clientY
        // console.log(x, y)
        let data_res = await this.editor_context.getContents()
        // console.log(data_res)
        let all_length = this.getWxDeltaLength(data_res.delta)
        // console.log('all_length:', all_length)
        // 二分法应该可以优化,规模小暂时不优化
        for (let i = 0; i < all_length; i++) {
            let bounds_res = await this.editor_context.getBounds({
                index: i,
                length: 1,
            })
            let bounds = bounds_res.bounds
            // console.log(bounds)
            if (bounds.left <= x && x <= bounds.left + bounds.width &&
                bounds.top <= y && y <= bounds.top + bounds.height
            ) {
                // console.log('click on index:', i)
                let item_type = this.getWxDeltaIndexType(data_res.delta, i)
                // console.log('click on type:', item_type)
                if (item_type.type != 'text') {
                    this.onClickItem(i, item_type.type, item_type.data, item_type)
                    break
                }
            }
        }
    }

最终html字符串的处理

需要把html中所有的img标签处理成对应的<a></a><span data-formula="" ></span><table></table>等等。

根据data-custom字段,例如data-custom="data=DJLFDSJFLK",提取里面的base64部分,然后解码回去,得到data数据:

    static decodeDataCustom(data) {
        if (!data) {
            return null
        }

        // console.log('decodeCustomData:', data)
        let data_str = data.substring('data='.length)
        // console.log(data_str)
        try {
            return JSON.parse(base64.decode(data_str))
        }
        catch (e) {
            console.log(e)
            return null
        }
    }

因为我用的Taro框架,对html转成dom有支持,所以这一部分实现还算简单。如果原生小程序可能需要进行正则匹配然后处理字符串。

此外,Editor导出html是上述的处理方式。导入html也需要对应的反向处理,将<a></a><span data-formula="" ></span><table></table>等等标签,再处理回img标签,此处不再展开。

最后一次编辑于  2天前  
点赞 1
收藏
评论
登录 后发表内容