小程序插入圖片
通過EditorContext.insertImage
接口可以實現圖片的插入:
EditorContext.insertImage({src,width,height,data,
})
如何插入超鏈接、公式、視頻、表格等等?
通過EditorContext.insertCustomBlock
應該是可以實現的,具體實現方式我沒有了解過,不過我使用的Taro3框架的版本,不支持渲染editor-portal
組件。
于是我通過EditorContext.insertImage
接口實現了這些功能。
思路是什么?
-
首先,渲染層面:公式可以通過Mathjax庫渲染成svg圖片;超鏈接可以通過svg圖片的形式渲染成文字圖片(如果嫌手動生成svg麻煩,也可以通過Mathjax渲染超鏈接的文字);表格可以通過svg圖片的形式渲染成表格圖片(如果嫌手動生成svg麻煩,也可以通過Mathjax渲染表格);視頻可以渲染一張封面圖或者一張默認圖片(點擊后可以修改視頻或者播放視頻等)
-
然后,數據層面:上述元素都是圖片,如何區分。可以通過
EditorContext.insertImage
中的data
字段來保存type
類型和相關屬性數據,最終會渲染到html的data-custom
中去。 -
其次,編輯層面:編輯器中刪除按鈕可以直接刪除圖片,然后如果實現點擊元素,彈出修改窗口,就可以實現上述元素的修改操作(Editor需要關閉show-img-size、show-img-toolbar、show-img-resize這三個參數),所以需要一種方案實現點擊能知道點擊的是哪個元素。
-
最后,生成的HTML:最終生成的HTML字符串中,上述元素都是img,需要通過
data-custom
中的type
字段來還原成對應的HTML字符串。
嘗試實踐:
插入元素
我以插入超鏈接為例:
async insertLink() {const { edit_index, link_text, link_url } = this.stateif (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()
}
extClass
用于給圖片加上class
,因為不能設置style
,目前只能通過這種方式給圖片加樣式。像超鏈接、公式圖片,因為需要和文字對齊,需要設置類似于vertical-align: -0.1em
這種(Mathjax生成的公式里有對應的屬性);然后文字圖片需要根據font-size
來進行縮放,需要設置類似于width: 3em
、height: 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);
src
的話,超鏈接、公式、表格等都是svg圖片,通過base64處理后傳給src
字段:
src: `data:image/svg+xml;base64,${base64.encode(svg_str)}`
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 } = deltaif (!ops) {return 0}let all_length = 0for (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 } = deltaif (!ops) {return {type: 'text',}}let now_index = 0for (let i = 0; i < ops.length; i++) {let item = ops[i]if (!item.insert) {continue }let old_index = now_indexif (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) {const { x, y } = e.detail// 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標簽,此處不再展開。