1. 簡介
這是一個chrome-extension,實現了一個增強文字復制的功能,我給它起名叫做Copy with URL。
它能夠將選中的純文本變身為超鏈接文本,你可以將超鏈接文本粘貼在其他支持插入超鏈接文本的編輯器中,比如Evernote,Word或者Excel。
有了Copy with URL,從此在需要插入超鏈接文本的時候,就不再需要復制文本>粘貼文本>復制鏈接>編輯鏈接+各種切換程序窗口的繁瑣操作。
如果你有頻繁使用超鏈接文本的習慣,那么它將極大提升你的工作效率,讓你更加專注於工作本身。
項目代碼見Copy-with-URL,涉及到的技術見文末總結。
2. 功能
使用它和正常的復制粘貼文本基本沒有區別,兩張圖來展示該擴展的使用方法和效果。
step1:選中網頁的純文本文字(文字原本是超鏈接文本也可以)后單機鼠標右鍵,在右鍵菜單中點擊Copy with URL選項。此時,文字及其所在網頁的URL就以超鏈接形式存儲在了系統剪貼板中。
step2:這就是將剛復制的超鏈接文本粘貼在word中的效果。受實現方式的限制,粘貼的內容消除了原本的字體等格式。
3. 項目闡述(相關工作)
該擴展源碼基於chrome-extension:Copy as Markdown Quotation進行改動開發,這里首先對原作者表示感謝!
Copy as Markdown Quotation實現了將純文本轉變為Markdown格式的超鏈接文本,粘貼在Word中的效果就是一段純文本后跟了一個本質上是文本的鏈接。
本擴展將純文本和URL組合成超鏈接文本,雖然只是一個小改動,但是對於我來說,還是廢了幾番周折,所以我將開發過程和所用到的技術記錄在此。
4. 實現
4.1 總體設計
需要明確的是:超鏈接文本和文本的區別在於超鏈接文本實際上是一個HTML頁的<a>標簽。知道了這一點后,開發的目標就定位成將在HTML頁內選定或創建的 <a> 標簽復制到剪貼板。其實,我之前很長時間都沒弄明白超鏈接文本和純文本的區別,以至於不知道如何下手。如果用純文本寫一段 <a href="url">value</a> ,把它復制到任一編輯器,得到的仍然是這段文本,因為這並不是一個DOM對象(不確定這樣說夠不夠准確)。
此擴展首先獲取選中對象,之后對選中的對象進行解析,提取出文本,標簽等信息。得到信息后,再將文本和URL組合成<a>標簽,最后將標簽復制到系統剪貼板。
js實現復制到剪貼板功能,兼容所有瀏覽器介紹了用js實現復制功能的多種方案,Copy as Markdown Quotation采用的是js自帶的 document.execCommand('copy');方法,但該方法貌似只能復制純文本內容,反正我沒有改動成功。
本工程使用的是clipboard.js框架,選它是因為看中它輕量級,易操作的特性。關鍵是它最終實現了復制帶鏈接的文本。
4.2 工程目錄
該工程包含的源代碼文件較少,所有的文件都在一個文件夾下。具體的文件有:
| manifest.json
| background.js
| background-cp.js
| clipboard.min.js
| icon16.png
| icon48.png
| icon128.png
4.3 manifest.json
manifest.json對於一個chrome-extension是很重要的文件,它的存在決定了extension的形態。
{ "background": { "scripts": [ "background-cp.js" ] }, "description": "Copy text in web page with URL", "icons": { "128": "icon128.png", "16": "icon16.png", "48": "icon48.png" }, "manifest_version": 2, "minimum_chrome_version": "24", "name": "Copy with URL", "permissions": [ "tabs", "storage", "contextMenus", "notifications", "file://*/","http://*/", "https://*/" ], "update_url": "https://clients2.google.com/service/update2/crx", "version": "0.4.1" }
在上面的manifest.json中,值得一提的是"permissions"中的contextMenus為該擴展獲取了操作瀏覽器右鍵菜單的權限。"background"屬性內的background-cp.js指示了該擴展在后台運行的js代碼,"background"還可以指定擴展運行在后台的html頁面。沒有指定html的話,瀏覽器會為extension創建一個空白的html頁。
4.4 backgroud-cp.js
該擴展的所有行為都定義在了background-cp.js內,代碼有點長,但只有幾個函數而已。
document.write("<script type='text/javascript' src='clipboard.min.js'></script>"); var get_selection = function() { var selection = document.getSelection(); var text = selection.toString(); var node = selection.getRangeAt(0).startContainer; var uri = node.baseURI || document.documentURI; var parent = node.parentElement; var whiteSpace = (parent && window.getComputedStyle(parent)['white-space']); var index; var ext; var is_code = function(elem) { if (!elem) return false; // Is the element monospace? if (window.getComputedStyle(elem)['white-space'].toLowerCase() == 'pre') { return true; } // Is the element generated by CodeMirror? if (elem.className.toLowerCase().split(' ').indexOf('codemirror') >= 0) { return true; } return false; } var pre = is_code(parent); console.log(pre) var get_frag = function(parent) { var frag, sibling, nephew; if (!parent) { return null; } frag = parent.id || parent.name; if (frag) { return frag; } sibling = parent.previousSibling; while(sibling) { frag = sibling.id || sibling.name; if (frag) { return frag; } nephew = sibling.children && sibling.children[0]; frag = nephew && (nephew.id || nephew.name); if (frag) { return frag; } sibling = sibling.previousSibling; } } var fileName; var orig_frag; var frag; // Remove the fragment from the url and find the better one only if the // original one was not semantically significant. index = uri.lastIndexOf('#'); console.log(index) orig_frag = index >= 0 ? uri.substring(index + 1) : null; if (!orig_frag || orig_frag.indexOf('/') < 0) { // Assume the fragment is siginificant if it contains '/'. uri = index >= 0 ? uri.substring(0, index) : uri; while(!frag && parent) { frag = get_frag(parent); parent = parent.parentElement; } } // Get extension from the url index = uri.lastIndexOf('/'); fileName = index >= 0 ? uri.substring(index + 1) : ''; index = fileName.lastIndexOf('.'); ext = index >= 0 ? fileName.substring(index + 1) : ''; if (frag) { uri += '#' + frag; } return { text: text, uri: uri, pre: pre, ext: ext }; } var ltrim_lines = function(str) { return str.replace(/^(\s*\n)*/, ''); } var rtrim = function(str) { return str.replace(/[\s\n]*$/, ''); } var copy_as_markdown_quot = function (args) { chrome.tabs.executeScript( { code: "(" + get_selection + ")();" }, function(selections) { var text = rtrim(ltrim_lines(selections[0].text)); var uri = selections[0].uri; var pre = selections[0].pre; var ext = selections[0].ext; if (text) { lines = text.split('\n'); result = ''; if (pre) result += '> ```' + ext + '\n'; for (var i = 0; i < lines.length; i++) { result += lines[i] + '\n'; } if (pre) result += '> ```\n' copyTextToClipboard(result,uri); } }); }; function copyTextToClipboard(text,uri) { var copyFrom,agent,body; copyFrom = document.createElement("a"); copyFrom.setAttribute("id","target"); copyFrom.setAttribute("href",uri); copyFrom.innerHTML = text; agent = document.createElement("button"); body = document.getElementsByTagName('body')[0]; body.appendChild(copyFrom); body.appendChild(agent); // 采用Clipboard.js方案 // trouble:沒有可以傳入的HTML元素,但我們可以動態創建一個DOM對象作為代理 var clipboard = new ClipboardJS(agent, { target: function() { return document.querySelector('#target'); } }); clipboard.on('success', function(e) { console.log(e); }); clipboard.on('error', function(e) { console.log(e); }); agent.click(); // copyFrom.focus(); // copyFrom.select(); // 問題所在 無法對copyFrom對象使用select()方法 // document.execCommand('copy'); // copy動態創建的<a>失敗 body.removeChild(copyFrom); body.removeChild(agent); } chrome.contextMenus.create({ title: "Copy with URL", contexts: ['selection'], onclick: copy_as_markdown_quot });
關於background-cp.js的幾點說明:
1) 代碼開頭的 document.write() 方法包含了clipboard框架,這樣就可以在background-cp.js內調用clipboard.js的方法。
2) chrome.contextMenus.create({...}); 在瀏覽器的右鍵菜單中創建子選項,點擊選項觸發 copy_as_markdown_quot() 函數。
3) chrome.tabs.executeScript({code:"..."},function(){...}) 可以在當前頁面執行一段嵌入式的js代碼,需要事先在"permissions"內聲明"tabs"權限。本例中,要在tab內執行的就是 get_selection() 函數,該方法解析出鼠標選中區域 document.getSelection(); 的文本及URL等信息,向回調函數傳入一個selections參數。
4) 回調函數對selection對象進行格式化處理,隨后調用 copyTextToClipboard() 函數,將格式化后的文本復制到剪貼板。
5) 對 copyTextToClipboard() 中復制方法的改動是本擴展的核心工作。該方法首先創建一個 <a> 標簽,該標簽實際上創建在隱藏的background.html頁中。然后實例化ClipboardJS對象復制 <a> 元素。
6) clipboard.js/demo/中給出了幾個應用clipboard.js的例子,本擴展所用的方法基於function-target.html進行改動。
clipboard需要傳入一個DOM selector,HTML element或者list of HTML elements來初始化。在function-target例中,點擊實例化ClipboardJS的button可以復制目標元素(帶格式),這也為實現本擴展目標提供了可能。
7) 問題在於無法在tab頁或者background.html頁找到一個合適的元素來初始化ClipboardJS對象,而且我也不想笨拙地通過點擊除右鍵菜單選項的方式來實現復制。
解決這個問題的方法是在background頁動態創建一個隱藏的button作為代理,如 agent = document.createElement("button"); 所示。除此之外, body.appendChild(agent); 將button嵌入到body當中。最后對該button調用一次 click()方法,復制生效,大功告成!
5. future
1) 計划加入清除文本格式的子菜單選項
2) 突破某些高冷網頁的復制限制 例如:百度文庫,知乎
6. 技術總結
本擴展實現了一個簡單的文本復制功能,應用到了這么幾項技術:
1) chrome-extension創建右鍵菜單選項
2) chrome-extension在tab頁執行一段js代碼
3) 動態創建HTML元素,設置元素屬性
4) 獲取並解析鼠標選中區域
5) Clipboard.js
6) 在一個JS文件內引入另一個JS文件內的方法
資料
【3】Copy as Markdown Quotation Chrome插件