系列文章
Svg Flow Editor 原生svg流程圖編輯器(一)
Svg Flow Editor 原生svg流程圖編輯器(二)
Svg Flow Editor 原生svg流程圖編輯器(三)
Svg Flow Editor 原生svg流程圖編輯器(四)
Svg Flow Editor 原生svg流程圖編輯器(五)
協(xié)同編輯
? ? ? ? 對協(xié)同這塊已經(jīng)寫了很多篇文章了,如果還是不了解,可以看看之前的文章哈,我們還是使用Yjs實現(xiàn)協(xié)同的底層支持,Websocket 還是以插件的形式支持:
? ? ? ? ?這次的協(xié)同,并沒有直接使用 y-websocket 插件支持,而是自己實現(xiàn)了websocket 相關(guān)的連接、異常、重連操作,y-websocket 插件無非就是內(nèi)部對協(xié)同數(shù)據(jù)做了合并,監(jiān)聽消息后觸發(fā) update更新:
?????????我們手動實現(xiàn),只需要對協(xié)同的數(shù)據(jù)進行底層的一致性沖突處理、合并就可以達到一樣的目的,如下:
? ? ? ? 在發(fā)送數(shù)據(jù)之前,需要先獲取本地的所有yjs數(shù)據(jù)狀態(tài) state,攜帶著一起發(fā)送給 websocket 服務(wù)器,其他客戶端收到后,先執(zhí)行解析合并操作,然后再從最終結(jié)果解析數(shù)據(jù),以達到數(shù)據(jù)一致性的目的。下列就是 yjs 的核心方法:
? ? ? ? 發(fā)送數(shù)據(jù)之前,進行數(shù)據(jù)映射:
? ? ? ? ?此類,我們就可以不基于 y-websocket插件,自身實現(xiàn)websocket服務(wù),也能使用yjs實現(xiàn)協(xié)同,保持?jǐn)?shù)據(jù)一致性,關(guān)鍵就是使用?encodeStateAsUpdate 進行本地數(shù)據(jù)獲取,applyUpdate 進行應(yīng)用更新,詳細(xì)解釋:
Document Updates | Yjs DocsHow to sync documents with other peers.https://docs.yjs.dev/api/document-updates#syncing-clients? ? ? ? 效果如下:
搜索替換
? ? ? ? 之前我們文本的實現(xiàn)方案是創(chuàng)建?contenteditable,然后移出時,創(chuàng)建了svg text,使得文本能顯示在元件上,但是這樣有些問題,不能進行搜索替換,因為svg的樣式與css樣式還不一致,因此在搜索結(jié)果的高亮顯示上還有些難以實現(xiàn)。
? ? ? ? 因此,我們替換方案為直接使用?contenteditable,移出時,控制樣式 point-event:none;user-select:none即可,在搜索高亮中,替換字符串為 b 標(biāo)簽,并加上css 控制,即可實現(xiàn)。
????????封裝搜索替換組件,并綁定快捷鍵 Ctrl + F
// 可以用 getSelection 獲取用戶目前選中的文本
const { anchorOffset, focusOffset, baseNode } = window.getSelection() as Selection;
// 搜索的核心就是遍歷目前頁面上的文本,判定內(nèi)容是否包含了搜索框文本
editorBox.querySelectorAll(".sf-editor-box-graphs-main-contenteditable").forEach((item) =>{
// item 是 contenteditableBox 里面的 div 才是內(nèi)容
const editor = item.querySelector("div") as HTMLDivElement;
editor.innerHTML = editor.innerHTML.replace(/<|>|\/|b|span/g, "");
const findFlag = editor.innerText.includes(this.keyword);
findFlag &&this.keyword &&this.conformList.push(item as HTMLDivElement);
});
? ? ? ? ?在 數(shù)量上,則是記錄全局變量 index all,all是搜索匹配到的所有文本項,index 則是匹配到的當(dāng)前索引,替換的方案就是直接 replace 即可,實現(xiàn)效果如下:
表格
? ? ? ? 本來想用 luckysheet 實現(xiàn)表格的,但是想了想,還是太冗余了,流程圖中的表格盡量簡單就好了,主要做數(shù)據(jù)展示,不涉及復(fù)雜的計算,因此,還是用原生的table 實現(xiàn)吧。
this.table = draw.createHTMLElement("table") as HTMLTableElement;
this.table.classList.add("sf-editor-table");
// 創(chuàng)建頭部 head
private createHead(draw: Draw) {
const thead = draw.createHTMLElement("thead");
const tr = draw.createHTMLElement("tr");
for (let i = 0; i < this.col; i++) {
const th = draw.createHTMLElement("th");
const div = draw.createHTMLElement("div");
div.innerText = `標(biāo)題${i + 1}`;
th.appendChild(div);
tr.appendChild(th);
}
thead.appendChild(tr);
this.table.appendChild(thead);
}
// 創(chuàng)建 tbody
private createBody(draw: Draw) {
const body = draw.createHTMLElement("tbody");
for (let i = 0; i < this.row; i++) {
const tr = draw.createHTMLElement("tr");
for (let i = 0; i < this.col; i++) {
const td = draw.createHTMLElement("td");
const div = draw.createHTMLElement("div");
td.appendChild(div);
tr.appendChild(td);
}
body.appendChild(tr);
}
this.table.appendChild(body);
}
? ? ? ? 文本編輯上,使用?contenteditable 實現(xiàn):
// 初始化 雙擊編輯事件
private initEvent() {
const divs = this.table.querySelectorAll("div");
divs.forEach((item) => {
item.addEventListener("dblclick", () => {
item.setAttribute("contenteditable", "true");
item.focus();
this.setRange(item);
item.addEventListener("blur", () =>
divs.forEach((i) => i.removeAttribute("contenteditable"))
);
});
});
}
?? ? ? ? 效果與markdown的表格類似:
圖片導(dǎo)出
? ? ? ? 導(dǎo)出使用的是html2canva庫,在一些細(xì)節(jié)的處理上,需要看官網(wǎng)的說明,比如處理跨域圖片問題,寬高尺寸問題,還有的就是循環(huán)遍歷導(dǎo)致截圖過慢問題等,可以看出,每次使用插件導(dǎo)出圖片,都會從 HTML head 開始遍歷DOM結(jié)構(gòu),在我們的項目中影響不大,但是用戶的環(huán)境,可能有很多的dom,肯定會影響效率,我們導(dǎo)出圖片僅需要在 sf-editor-box 中做處理即可,因此,需要使用?ignoreElements 進行元素過濾。
? ? ? ? 沒有做過濾,整體的時間大概在435毫秒:
const option = {
ignoreElements: (ele: HTMLElement) => {
// this.editorBox compareDocumentPosition
// 1: 沒有關(guān)系,這兩個節(jié)點不屬于同一個文檔
// 2: 第一節(jié)點(P1)位于第二個節(jié)點后(P2)
// 4: 第一節(jié)點(P1)定位在第二節(jié)點(P2)前
// 8: 第一節(jié)點(P1)位于第二節(jié)點內(nèi)(P2)
// 16:第二節(jié)點(P2)位于第一節(jié)點內(nèi)(P1)
// 還可能是上訴值的和!返回 20 意味著在 p2 在 p1 內(nèi)部(16),并且 p1 在 p2 之前(4)
const box = this.draw.getEditorBox();
const index = box.compareDocumentPosition(ele);
if ([1, 2, 4].includes(index)) return false;
},
};
? ? ? ? 優(yōu)化后的平均耗時 250毫秒,如果在大體量DOM結(jié)構(gòu)中,這個優(yōu)化會更加明顯。
/**
* 利用 html2canvas 截圖
* 1. ignoreElements 處理截圖慢問題: (element) => false 與 root 進行位置比較
* 2. x y width height 處理最佳寬高,不出現(xiàn)大量空白
* 3. proxy、useCORS、allowTaint 處理跨域圖片問題
* 4. backgroundColor 支持透明、白色背景(設(shè)置null為透明)
* @param filetype 保存的文件類型,支持 png svg jpg json
*/
public async screenShot(filetype: string) {
await nextTick();
const box = this.draw.getEditorBox();
// const width = box.clientWidth;
// const height = box.clientHeight;
this.draw.showLoading();
// 處理x y height width - 相對于 editor box 的位置關(guān)系
var minx = 0;
var miny = 0;
var maxx = 0;
var maxy = 0;
// 獲取 editor box 的寬高
const graphlist = this.draw.getGraphEvent().getAllGraphMain();
if (graphlist.length) {
const firstGraph = new Graph(
this.draw,
graphlist[0].getAttribute("graphid") as string
);
minx = firstGraph.getX();
miny = firstGraph.getY();
graphlist.forEach((item) => {
// 需要得到最小和最大位置的graph
const nodeID = item.getAttribute("graphid") as string;
const graph = new Graph(this.draw, nodeID);
minx = Math.min(minx, graph.getX());
miny = Math.min(miny, graph.getY());
maxx = Math.max(maxx, graph.getX() + graph.getWidth() + 20);
maxy = Math.max(maxy, graph.getY() + graph.getHeight() + 20);
});
}
const option = {
x: minx,
y: miny,
width: maxx - minx,
height: maxy - miny,
ignoreElements: (ele: HTMLElement) => {
// this.editorBox compareDocumentPosition
// 1: 沒有關(guān)系,這兩個節(jié)點不屬于同一個文檔
// 2: 第一節(jié)點(P1)位于第二個節(jié)點后(P2)
// 4: 第一節(jié)點(P1)定位在第二節(jié)點(P2)前
// 8: 第一節(jié)點(P1)位于第二節(jié)點內(nèi)(P2)
// 16:第二節(jié)點(P2)位于第一節(jié)點內(nèi)(P1)
// 還可能是上訴值的和!返回 20 意味著在 p2 在 p1 內(nèi)部(16),并且 p1 在 p2 之前(4)
const index = box.compareDocumentPosition(ele);
if ([1, 2, 4].includes(index)) return false;
},
};
// @ts-ignore
const canvas = await html2canvas(this.draw.getEditorBox(), option);
// base64 使用服務(wù)器存儲方案 const base64 = canvas.toDataURL("image/png");
canvas.toBlob((b: File) => {
const url = toBlob(b, "image/png") as string;
const a = this.draw.createHTMLElement("a");
a.setAttribute("href", url);
a.setAttribute("download", "測試");
this.draw.hideLoading();
window.open(url);
// a.click(); // 觸發(fā)下載
a.remove();
});
}
總結(jié)
? ? ? ? 至此,該實現(xiàn)的功能基本上都已經(jīng)具備雛形了,后面就不再更新文章咯,但是還是會持續(xù)更新這個庫,大家有什么想法,需要什么BUG,都可以在git、文章下留言,我會持續(xù)關(guān)注大家的意見,維護這個庫。
? ? ? ? 即將發(fā)布的 1.0.15 版本,是1.0版本的最后一版,后續(xù)的版本將更替為 1.1 ,主要實現(xiàn)協(xié)同、相關(guān)工具類、以及關(guān)鍵的 history歷史記錄。目前市面上也有很多成熟的產(chǎn)品,做這個主要不是為了超越他們,而是熟悉流程圖的底層實現(xiàn)、TypeScript的應(yīng)用、以及主要的提升自我能力,望大家理性看待~文章來源:http://www.zghlxwxcb.cn/news/detail-849143.html
? ? ? ? 感謝大家的支持與理解!文章來源地址http://www.zghlxwxcb.cn/news/detail-849143.html
到了這里,關(guān)于Svg Flow Editor 原生svg流程圖編輯器(五)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!