大家好,我是前端西瓜哥。
今天這篇文字來講解一下圖形編輯器如何實現圖形的復制粘貼。
首先需要確認一下粘貼的范圍。
如果只支持粘貼到當前編輯器下,方案很簡單:只需要監聽 Ctrl + C
鍵盤事件深拷貝一份選中圖形對象,然后再監聽 Ctrl + V
事件,將拷貝出來的對象添加到圖形樹的末尾。
但通常我們希望可以跨 tab 頁,跨圖紙,跨瀏覽器,甚至從 Web 端復制到桌面端。
很明顯,要實現這樣的場景,我們需要操作系統級的支持:剪貼板。
我們看看怎么實現通過剪貼板實現圖形的復制粘貼。
先是復制邏輯。
復制通常為兩種方式:
如下圖:
當調用復制命令時,我們要將 選中的圖形生成序列化快照。
所謂序列化,就是將內存中的對象轉換為可以持久化的數據。最簡單快捷的就是用 JSON.stringify() 序列化為 JSON 字符串。
除了圖形對象 data,我們還要保存一些必要的元信息。
最后我們要保存的信息有:
/** * 生成選中圖形的快照,并保存到操作系統剪貼板中 */const getSelectedItemsSnapshot() => { const selectedItems = selectSet.getItems(); if (selectedItems.length === 0) { return null; } // 提取圖形原始屬性,丟掉多余屬性(比如 id) const copiedData = arrMap(selectedItems, (item) => lodash.omit(item.getAttrs(), 'id'), ); // 序列化 return JSON.stringify({ appVersion: this.editor.appVersion, paperId: this.editor.paperId, data: JSON.stringify(copiedData), });}
拿到快照信息后,我們會調用 navigator.clipboard.writeText() 方法,將數據保存到操作系統的剪貼板中。
/** * 綁定 Ctrl/Cmd + C 的事件響應函數 */const copyHandler = () => { const snapshot = getSelectedItemsSnapshot(); if (!snapshot) { return; } // 將序列化結果保存到剪貼板 navigator.clipboard.writeText(snapshot).then(() => { // 這里可以考慮加一個 “復制成功” 彈窗提示 console.log('copied'); });};hotkeys('cmd+c, ctrl+c', copyHandler);
然后就是粘貼了。
粘貼分為右鍵粘貼和快捷鍵粘貼。
這里的右鍵粘貼使用了 clipboard.readText() 方法。因為該方法不是用戶的主動動作,涉及到用戶隱私問題,所以需要用戶授權剪貼板權限才行。
另外,Firefox 瀏覽器直接報錯,不會彈出剪貼板授權彈窗。
這不是個技術問題,因為可以手動修改 Firefox 瀏覽器設置啟用剪貼板授權。它更是一個安全問題,Firefox 不認為用戶能夠正確地授權粘貼板操作,以及開發者不會濫用這個權限收集用戶隱私。
右鍵粘貼因為提供了光標位置,所以我們可以將圖形的位置對上這個位置。
前面我們因為主動獲取剪貼板的內容,所以有權限問題。
但如果我們監聽用戶的 “粘貼” 操作,權限就寬松了很多,不需要授權。
因為這是用戶的主動行為,用戶從剪貼板取出了數據交給你,而不是你主動去訪問剪貼板的數據。
const pasteHandler = (e: Event) => { const event = e as ClipboardEvent; const clipboardData = event.clipboardData; if (!clipboardData) { return; } // 拿到粘貼的文本內容 const pastedData = clipboardData.getData('Text'); // ...};// 監聽 “粘貼” 事件window.addEventListener('paste', pasteHandler);
如果用戶拒絕授權,我們可以考慮提示用戶 “用 Ctrl + C 的方式粘貼”,或者用用戶上次右鍵粘貼的內容湊數,雖然可能貨不對版,但好歹有個東西。
快捷鍵粘貼沒有光標操作,所以粘貼圖形的位置需要用另一種方式去處理。
我們需要考慮兩種情況:相同圖紙和跨圖紙。
對于在同一個圖紙下快捷鍵粘貼,圖形復制時在哪里,粘貼也在哪里。
或者你可以給一個小的右下偏移,讓用戶感知到粘貼成功了。我個人不喜歡這個偏移,因為通常我復制,就是為了讓圖形做重復對齊排列的,我還得給它移動回去。
如果是在另一張圖紙下粘貼,我們就不能這么做了。
為什么呢?
舉個例子,假設用戶復制了圖紙 A 中在 (10000, 10000) 坐標的圖形。然后我打開圖紙 B,圖紙 B 此時視口的中心坐標在 (0, 0)。
用戶一粘貼,然后說,誒,粘貼的圖形哪去了?你說我可以讓視口移動到粘貼圖形的位置,那用戶會說,誒,我在哪里,我的其他圖形哪去了?
所以 對于跨圖紙場景,最好的做法是將圖形粘貼到畫布正中心。
代碼邏輯有點多,就不文字敘述了,看代碼里面的注釋吧。
class ClipboardManager { private unbindEvents = noop; constructor(private editor: Editor) {} bindEvents() { // Ctrl+C 鍵盤事件響應函數 const copyHandler = () => { this.copy(); }; // 粘貼事件響應函數 const pasteHandler = (e: Event) => { const event = e as ClipboardEvent; const clipboardData = event.clipboardData; if (!clipboardData) { return; } const pastedData = clipboardData.getData('Text'); this.addGraphsFromClipboard(pastedData); }; hotkeys('cmd+c, ctrl+c', copyHandler); window.addEventListener('paste', pasteHandler); this.unbindEvents = () => { hotkeys.unbind('cmd+c, ctrl+c', copyHandler); window.removeEventListener('paste', pasteHandler); }; } /** * 將快照保存到剪貼板 */ copy() { const snapshot = this.getSelectedItemsSnapshot(); if (!snapshot) { return; } navigator.clipboard.writeText(snapshot).then(() => { console.log('copied'); }); } pasteAt(x: number, y: number) { navigator.clipboard.readText().then((pastedData) => { this.addGraphsFromClipboard(pastedData, x, y); }); } /** * 生成選中圖形的快照(序列化) */ private getSelectedItemsSnapshot() { const selectedItems = this.editor.selectedElements.getItems(); if (selectedItems.length === 0) { return null; } const copiedData = arrMap(selectedItems, (item) => omit(item.getAttrs(), 'id'), ); return JSON.stringify({ appVersion: this.editor.appVersion, paperId: this.editor.paperId, data: JSON.stringify(copiedData), }); } // 在指定坐標位置粘貼內容 private addGraphsFromClipboard(dataStr: string): void; private addGraphsFromClipboard(dataStr: string, x: number, y: number): void; private addGraphsFromClipboard(dataStr: string, x?: number, y?: number) { let pastedData: IEditorPaperData | null = null; try { // 反序列化 pastedData = JSON.parse(dataStr); } catch (e) { return; } // 數據格式校驗 if ( !( pastedData && pastedData.appVersion.startsWith('suika-editor') && pastedData.data ) ) { return; } const editor = this.editor; // 將數據解析并添加到圖形樹中 const pastedGraphs = editor.sceneGraph.addGraphsByStr(pastedData.data); if (pastedGraphs.length === 0) { return; } // 添加到歷史記錄(以實現撤銷重做) editor.commandManager.pushCommand( new AddShapeCommand('pasted graphs', editor, pastedGraphs), ); // 標記粘貼圖形為選中狀態 editor.selectedElements.setItems(pastedGraphs); const bbox = editor.selectedElements.getBBox()!; // 如果是右鍵粘貼(x 和 y 沒有值)且跨圖紙粘貼,計算粘貼圖形要移動的目標位置 if ( (x === undefined || y === undefined) && pastedData.paperId !== editor.paperId ) { const vwCenter = this.editor.viewportManager.getCenter(); x = vwCenter.x - bbox.width / 2; y = vwCenter.y - bbox.height / 2; } // 遍歷粘貼圖形,根據 x 和 y 進行位置修正 if (x !== undefined && y !== undefined) { const dx = x - bbox.x; const dy = y - bbox.y; if (dx || dy) { Graph.dMove(pastedGraphs, dx, dy); } } // 渲染畫布 editor.sceneGraph.render(); } // 銷毀時解綁事件監聽 destroy() { this.unbindEvents(); }}
這里補充一些可以優化的點。
前面的實現其實有個用戶體驗不好的地方,就是用戶復制后,在圖形編輯器外粘貼,會粘貼出一堆意義不明的字符串。
最好是用戶粘貼不出任何東西,這個有辦法解決。
之前我們用的是 clipboard.writeText() 方法,給數據指定的是 text/plain 的 MIME 類型。
實際上我們可以用另一個方法 clipboard.write(),該方法可以指定其他的文本相關 MIME 類型,然后將我們真正的數據放到到一些不會被其他軟件解析的角落里。
我們來看看隔壁 Figma 是怎么做的?它將復制的數據設置為 text/html 類型。
我再看看它的 HTML 都是什么內容。
可以看到數據主要保存在兩個 span 元素上,它們都沒有文本內容,所以在文本編輯器中進行標準的粘貼是粘貼不出任何內容的。
但這里 Figma 巧妙地用了一個自定義的 data-metadata 和 data-buffer 去保存真正的數據。這個數據看著像是序列化后的類似 base64 格式的內容。
這樣就能巧妙地防止其他文本編輯器能夠粘貼出內容,自己的編輯器卻會在解析 html 結構時特意去讀這個自定義屬性拿到數據。
代碼實現大概為:
const blob = new Blob( [ `<meta charset="utf-8"> <span data-suika-meta="${這里是元數據}"></span> <span data-suika-data="${這里是主體數據}"><span>`, ], { type: 'text/html' },);navigator.clipboard .write([new ClipboardItem({ [blob.type]: blob })]) .then(() => { console.log('copied'); });
Firefox 目前(2023.08.06)不支持 ClipboardItem,需要 document.execCommand('copy') 的舊方法來兼容。
如果要用 text/html 這種方式,還要做多幾個工作:
然后就是粘貼文字、圖形的情況,這時我們就不能用 clipboard.writeText(),要用 clipboard.write() 了。
總結一下圖形編輯器的圖形復制粘貼的邏輯。
在復制時,要將選中圖形進行序列化保存到剪貼板。
粘貼的場景就比較多了。粘貼時需要反序列化解析數據,并創建對象添加到圖形樹上。
粘貼要注意權限問題,快捷鍵粘貼權限比較寬松,不需要用戶授權;右鍵粘貼則因為是開發者的主動行為,所以需要授權,如果用戶不授權,可以考慮提示用戶用快捷鍵粘貼的方式,或粘貼上一次快捷鍵粘貼的內容。
右鍵粘貼時需要將圖形粘貼到光標位置上。快捷鍵粘貼時則需要考慮是否跨圖紙,如果是相同圖紙,原地粘貼即可;如果是另一張圖紙,則粘貼到視口正中心。
本文鏈接:http://www.tebozhan.com/showinfo-26-11894-0.html圖形編輯器開發:實現圖形的復制粘貼
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com
上一篇: 好用!這些工具國慶一定要研究下
下一篇: c#委托用法詳解,你了解嗎?