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文件内的方法
资料