html小工具——文章註釋編輯器
在網上閱讀文章時,讀者時常會想針對某段文字寫一些自己的感想,可惜大部分閱讀網站並不提供這樣的功能,讀者往往只能將文字複製到本地或線上的編輯器中編輯註釋,之後如果想在其他地方回顧這些註釋也必須先本地安裝或聯網登入(雲筆記)編輯器。如此操作,麻煩倒在其次,錯過靈感才是最讓人惋惜的,於是決定編寫一個簡單的小工具嘗試解決這一問題,
一、用法:
用Chrome瀏覽器開啟此工具
點選“選擇文字檔案”選擇txt檔案,或者直接將純文字貼上到文字框中,點選確定則以綠色背景顯示出文字內容,滑鼠雙擊綠色正文段落即在下面彈出黃色註釋段落,雙擊註釋段落即可編輯註釋內容(正文不可編輯),編輯時點選其他地方即可取消編輯,再次雙擊綠色正文段落則隱藏本段註釋。
點選匯出html即可將正文和註釋內容儲存為一個html文件,用Chrome瀏覽器開啟此html文件即可以檢視已經錄入的正文和註釋,另外這個匯出的html文件也具有本工具的所有特性,使用者可以繼續編輯註釋內容,或者完全替換性的載入新的文字,編輯完畢後又可以匯出為新的html文件。
二、編寫方法:
1、首先拼一個html結構出來:
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>實用版,處理事件問題</title> 6 <style> 7 /*通用屬性*/ 8 body{ margin: 0; padding: 0; border: 0; text-align: center; overflow: hidden;width: 100%; 9 height: 100%;position: fixed; font-family: verdana,arial,sans-serif; touch-action: none; 10 -ms-touch-action: none;font-size: 12px;min-width: 600px;} 11 ul { list-style: none; margin: 0; padding: 0;} 12 li{ list-style: none; margin: 0; padding: 0;} 13 ul li { float: left;} 14 button{ cursor: pointer; height: 23px;} 15 a:link{ text-decoration: none;} 16 17 #div_top span,input,button{margin-left: 20px;float: left;} 18 .p_h1{font-size: 24px;font-weight: bolder;background-color: darkseagreen} 19 .p_text{font-size: 20px;font-weight: normal;text-align: left;background-color: darkseagreen} 20 21 .div_section{position:relative} 22 .p_a1{font-size: 20px;font-weight: normal;text-align: left;background-color: beige;margin-left: 60px;margin-right: 60px; 23 ;min-height: 80px;display: block;word-break: break-all;overflow-wrap: break-word;white-space: pre-wrap} 24 .p_a2{font-size: 20px;font-weight: normal;text-align: left;background-color: beige;margin-left:120px} 25 </style> 26 </head> 27 <body> 28 <div id="div_allbase" style="top:0px;bottom:0px;width:100%;height: 100%;position:relative;overflow-x: hidden;overflow-y: scroll 29 "> 30 <div id="div_top" style="top:0px;left:0px;width:100%;height: 30px;position:absolute;"> 31 <span style="margin-left: 20px">選擇文字檔案</span> 32 <input type="file" id="str_local" onchange="ShowStr()" style="margin-left: 20px"> 33 <span style="margin-left: 20px">貼上文字</span> 34 <input type="text" id="str_local2" ><button onclick="ShowStr2()">確定</button> 35 <button onclick="ExportHtml()">匯出Html</button> 36 </div> 37 <div id="div_main" style="top:30px;left:0px;width:100%;position:absolute;"> 38 39 </div> 40 </div> 41 </body>
其中div_top是上面的操作區,div_main用來存放正文段落和註釋段落。
2、將純文字轉換為正文段落:
1 function ShowStr()//從檔案讀取文字 2 { 3 var str_local=document.getElementById("str_local"); 4 var file=str_local.files[0]; 5 var reader=new FileReader(); 6 reader.readAsText(file); 7 reader.onload=function(e) 8 { 9 var str=e.target.result; 10 loadArticle(str); 11 } 12 } 13 function ShowStr2()//從文字框讀取文字 14 { 15 var str_local=document.getElementById("str_local2"); 16 var str=str_local.value; 17 loadArticle(str); 18 } 19 var currentSectionid=null;//當前選中的文字段落id 20 function loadArticle(str)//將文字轉為段落 21 { 22 var div_main=document.getElementById("div_main"); 23 div_main.innerHTML=""; 24 var arr_section=str.split("\r\n");//分段 25 var len=arr_section.length; 26 var count_p=0;//包含主標題在內一共分成幾段 27 var arr_p=[]; 28 for(var i=0;i<len;i++) 29 { 30 var section=arr_section[i]; 31 if(section.length>0)//如果本段非空 32 { 33 let div_section=document.createElement("div"); 34 div_section.className="div_section";//這樣可以更方便的在段內插入元素 35 div_section.id="div_section_"+count_p; 36 37 let p=document.createElement("p"); 38 if(count_p==0)//標題段 39 { 40 p.className="p_h1"; 41 } 42 else 43 { 44 p.className="p_text"; 45 } 46 p.innerHTML=" "+section; 47 p.id="p_section_"+count_p; 48 49 p.ondblclick=function()//雙擊正文段落時展開註釋段落 50 { 51 addAnnotate(div_section.id); 52 } 53 count_p++; 54 div_section.appendChild(p); 55 div_main.appendChild(div_section); 56 } 57 } 58 }
程式碼 並不複雜,根據\r\n對文字進行分段,每一段生成對應的html文件插入dom中,每個“section”都有唯一的dom id。目前還沒有除錯好的是如何在匯出匯入html時保持每一段前面的四個空格,瀏覽器會自動將它們去掉。
注意在為批量建立的標籤設定事件響應時,需要用let型變數而非var型變數,否則同一迴圈中定義的所有事件響應都將以迴圈操作的最後一個標籤為目標。
3、雙擊正文段落時展開註釋段落:
1 function addAnnotate(id) 2 { 3 var div_section=document.getElementById(id); 4 currentSectionid=id;//這個好像沒用到 5 var children=div_section.childNodes; 6 var len=children.length; 7 if(len==1)//此時還沒有第一級註釋 8 { 9 let p_a1=document.createElement("p"); 10 p_a1.className="p_a1"; 11 //點選第一級註釋,進行編輯 12 p_a1.ondblclick=function(){openEdit(p_a1)}; 13 p_a1.onblur=function(){closeEdit(p_a1)};//失去焦點時關閉註釋段的編輯狀態 14 div_section.appendChild(p_a1) 15 } 16 else 17 { 18 if(children[1].style.display=="none")//如果當前是隱藏狀態 19 { 20 for(var i=1;i<len;i++) 21 { 22 children[i].style.display="block";//顯示註釋段 23 } 24 } 25 else 26 { 27 for(var i=1;i<len;i++) 28 { 29 var child=children[i]; 30 child.style.display="none";//隱藏註釋段 31 if(child.className=="p_a1") 32 { 33 closeEdit(child); 34 } 35 } 36 } 37 } 38 }
這裡根據sectionid找到當前操作的正文段落div,將空白的註釋段落插進去。
4、編輯註釋及關閉編輯狀態:
1 function openEdit(p) 2 { 3 p.style.border="2px solid cornflowerblue"; 4 p.style.borderRadius="5px"; 5 p.contentEditable="true"; 6 } 7 function closeEdit(p) 8 { 9 p.style.border="0px"; 10 p.contentEditable="false"; 11 //Chrome的預設編輯模式會在p內插入一個div,這個div是用來分行的,空白的換行也會導致div!! 12 // 但在重新匯入之後(在取innerHTML匯出時尚正常)瀏覽器會自動把這個div繪製在p的外面!!!! 13 //多次換行是巢狀的!!!!所以簡單的替換還不行!! 14 p.innerHTML=p.innerHTML.replace(new RegExp(("<[^>]+>"),"gm"),"@");//如果有連續的多個xml標籤 15 p.innerHTML=p.innerHTML.replace(new RegExp(("[@]+"),"gm"),"\r\n"); 16 //p.innerHTML=p.innerHTML.replace(new RegExp(("<[^>]+>"),"gm"),"\r\n"); 17 //p.innerHTML=p.innerHTML.replace(new RegExp(("<[^/][\\S]{1,5}>"),"gm"),"\r\n"); 18 //p.innerHTML=p.innerHTML.replace(new RegExp(("</[\\S]{1,6}>"),"gm"),""); 19 }
這裡使用了瀏覽器內建的“contentEditable”屬性編輯p標籤內容,這樣做的一個缺點是瀏覽器會在使用者編輯標籤時不受使用者控制的插入各種控制格式的標籤,而同樣的html再次匯入瀏覽器時,又會按照不同的規則自動排列成別的順序。(類似瀏覽器的CSS除錯,只預期在執行時一次性使用?)
為解決這一問題,我在關閉p標籤的contentEditable狀態時,用正則表示式清除了所有的xml標籤,但這也將導致新的問題——使用者希望寫在註釋中的xml標籤(包括替換符號@)也會被清除掉,不知道有沒有更好的方法解決這一問題。
5、匯出html文件:
1 function ExportHtml() 2 { 3 var str=str_head+window.document.body.outerHTML+"</html>"; 4 var blob=new Blob([str],{//字串轉為二進位制流 5 type: "text/plain" 6 }) 7 8 var tmpa = document.createElement("a"); 9 var p_h1=document.getElementsByClassName("p_h1")[0]; 10 tmpa.download = (p_h1?p_h1.innerHTML:"test")+".html";//自動用標題作為檔名 11 tmpa.href = URL.createObjectURL(blob);//用臨時的a標籤下載 12 tmpa.click();//匯出後事件需要重新繫結,或者直接使用innHTML定義? 13 setTimeout(function () { 14 URL.revokeObjectURL(blob);//釋放掉流 15 }, 100); 16 }
其中str_head包含了html頭部內容,加上目前正在編輯的body內容和html結束標籤組成匯出的html文件,然後轉換為二進位制檔案流用臨時a標籤下載。
注意,使用.onxx方式繫結的事件並不會隨著html文件一起匯出,可以選擇在開啟html時重新繫結事件,或者在建立標籤時使用innerHTML而非appendChild方法,直接將事件響應寫在innerHTML裡
6、重新開啟匯出的html文件時重新繫結事件:
1 window.onload=function(){ 2 //處理匯出html的事件有兩種思路,一是使用innerHTML定義所有動態生成的標籤,二是在每次開啟網頁時重新繫結事件 3 var div_main=document.getElementById("div_main"); 4 var arr_section=div_main.getElementsByClassName("div_section"); 5 var len=arr_section.length; 6 for(var i=0;i<len;i++) 7 { 8 let div_section=arr_section[i]; 9 var arr_p=div_section.getElementsByTagName("p"); 10 var len2=arr_p.length; 11 for(var j=0;j<len2;j++) 12 { 13 let p=arr_p[j]; 14 if(j==0) 15 { 16 p.ondblclick=function() 17 { 18 addAnnotate(div_section.id); 19 } 20 } 21 else 22 { 23 p.ondblclick=function(){openEdit(p)}; 24 p.onblur=function(){closeEdit(p)}; 25 } 26 } 27 } 28 }
如此就完成了文章註釋編輯器小工具的編寫,下面是完整的程式碼,複製貼上到一個空白的.html
檔案中即可使用。
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>實用版,處理事件問題</title> 6 <style> 7 /*通用屬性*/ 8 body{ margin: 0; padding: 0; border: 0; text-align: center; overflow: hidden;width: 100%; 9 height: 100%;position: fixed; font-family: verdana,arial,sans-serif; touch-action: none; 10 -ms-touch-action: none;font-size: 12px;min-width: 600px;} 11 ul { list-style: none; margin: 0; padding: 0;} 12 li{ list-style: none; margin: 0; padding: 0;} 13 ul li { float: left;} 14 button{ cursor: pointer; height: 23px;} 15 a:link{ text-decoration: none;} 16 17 #div_top span,input,button{margin-left: 20px;float: left;} 18 .p_h1{font-size: 24px;font-weight: bolder;background-color: darkseagreen} 19 .p_text{font-size: 20px;font-weight: normal;text-align: left;background-color: darkseagreen} 20 21 .div_section{position:relative} 22 .p_a1{font-size: 20px;font-weight: normal;text-align: left;background-color: beige;margin-left: 60px;margin-right: 60px; 23 ;min-height: 80px;display: block;word-break: break-all;overflow-wrap: break-word;white-space: pre-wrap} 24 .p_a2{font-size: 20px;font-weight: normal;text-align: left;background-color: beige;margin-left:120px} 25 </style> 26 </head> 27 <body> 28 <div id="div_allbase" style="top:0px;bottom:0px;width:100%;height: 100%;position:relative;overflow-x: hidden;overflow-y: scroll 29 "> 30 <div id="div_top" style="top:0px;left:0px;width:100%;height: 30px;position:absolute;"> 31 <span style="margin-left: 20px">選擇文字檔案</span> 32 <input type="file" id="str_local" onchange="ShowStr()" style="margin-left: 20px"> 33 <span style="margin-left: 20px">貼上文字</span> 34 <input type="text" id="str_local2" ><button onclick="ShowStr2()">確定</button> 35 <button onclick="ExportHtml()">匯出Html</button> 36 </div> 37 <div id="div_main" style="top:30px;left:0px;width:100%;position:absolute;"> 38 39 </div> 40 </div> 41 </body> 42 <script> 43 44 window.onload=function(){ 45 //處理匯出html的事件有兩種思路,一是使用innerHTML定義所有動態生成的標籤,二是在每次開啟網頁時重新繫結事件 46 var div_main=document.getElementById("div_main"); 47 var arr_section=div_main.getElementsByClassName("div_section"); 48 var len=arr_section.length; 49 for(var i=0;i<len;i++) 50 { 51 let div_section=arr_section[i]; 52 var arr_p=div_section.getElementsByTagName("p"); 53 var len2=arr_p.length; 54 for(var j=0;j<len2;j++) 55 { 56 let p=arr_p[j]; 57 if(j==0) 58 { 59 p.ondblclick=function() 60 { 61 addAnnotate(div_section.id); 62 } 63 } 64 else 65 { 66 p.ondblclick=function(){openEdit(p)}; 67 p.onblur=function(){closeEdit(p)}; 68 } 69 } 70 } 71 } 72 function ShowStr() 73 { 74 var str_local=document.getElementById("str_local"); 75 var file=str_local.files[0]; 76 var reader=new FileReader(); 77 reader.readAsText(file); 78 reader.onload=function(e) 79 { 80 var str=e.target.result; 81 loadArticle(str); 82 } 83 } 84 function ShowStr2() 85 { 86 var str_local=document.getElementById("str_local2"); 87 var str=str_local.value; 88 loadArticle(str); 89 } 90 var currentSectionid=null; 91 function loadArticle(str) 92 { 93 var div_main=document.getElementById("div_main"); 94 div_main.innerHTML=""; 95 var arr_section=str.split("\r\n"); 96 var len=arr_section.length; 97 var count_p=0;//包含主標題在內一共分成幾段 98 var arr_p=[]; 99 for(var i=0;i<len;i++) 100 { 101 var section=arr_section[i]; 102 if(section.length>0) 103 { 104 let div_section=document.createElement("div"); 105 div_section.className="div_section";//這樣可以更方便的在段內插入元素 106 div_section.id="div_section_"+count_p; 107 108 let p=document.createElement("p"); 109 if(count_p==0)//標題段 110 { 111 p.className="p_h1"; 112 } 113 else 114 { 115 p.className="p_text"; 116 } 117 p.innerHTML=" "+section; 118 p.id="p_section_"+count_p; 119 120 p.ondblclick=function() 121 { 122 addAnnotate(div_section.id); 123 } 124 count_p++; 125 div_section.appendChild(p); 126 div_main.appendChild(div_section); 127 } 128 } 129 } 130 function addAnnotate(id) 131 { 132 var div_section=document.getElementById(id); 133 currentSectionid=id; 134 var children=div_section.childNodes; 135 var len=children.length; 136 if(len==1)//此時還沒有第一級註釋 137 { 138 let p_a1=document.createElement("p"); 139 p_a1.className="p_a1"; 140 //點選第一級註釋,進行編輯 141 p_a1.ondblclick=function(){openEdit(p_a1)}; 142 p_a1.onblur=function(){closeEdit(p_a1)}; 143 div_section.appendChild(p_a1) 144 } 145 else 146 { 147 if(children[1].style.display=="none")//如果當前是隱藏狀態 148 { 149 for(var i=1;i<len;i++) 150 { 151 children[i].style.display="block"; 152 } 153 } 154 else 155 { 156 for(var i=1;i<len;i++) 157 { 158 var child=children[i]; 159 child.style.display="none"; 160 if(child.className=="p_a1") 161 { 162 closeEdit(child); 163 } 164 } 165 } 166 } 167 } 168 function openEdit(p) 169 { 170 p.style.border="2px solid cornflowerblue"; 171 p.style.borderRadius="5px"; 172 p.contentEditable="true"; 173 } 174 function closeEdit(p) 175 { 176 p.style.border="0px"; 177 p.contentEditable="false"; 178 //Chrome的預設編輯模式會在p內插入一個div,這個div是用來分行的,空白的換行也會導致div!! 179 // 但在重新匯入之後(在取innerHTML匯出時尚正常)瀏覽器會自動把這個div繪製在p的外面!!!! 180 //多次換行是巢狀的!!!!所以簡單的替換還不行!! 181 p.innerHTML=p.innerHTML.replace(new RegExp(("<[^>]+>"),"gm"),"@"); 182 p.innerHTML=p.innerHTML.replace(new RegExp(("[@]+"),"gm"),"\r\n"); 183 //p.innerHTML=p.innerHTML.replace(new RegExp(("<[^>]+>"),"gm"),"\r\n"); 184 //p.innerHTML=p.innerHTML.replace(new RegExp(("<[^/][\\S]{1,5}>"),"gm"),"\r\n"); 185 //p.innerHTML=p.innerHTML.replace(new RegExp(("</[\\S]{1,6}>"),"gm"),""); 186 } 187 188 function ExportHtml() 189 { 190 var str=str_head+window.document.body.outerHTML+"</html>"; 191 var blob=new Blob([str],{ 192 type: "text/plain" 193 }) 194 195 var tmpa = document.createElement("a"); 196 var p_h1=document.getElementsByClassName("p_h1")[0]; 197 tmpa.download = (p_h1?p_h1.innerHTML:"test")+".html"; 198 tmpa.href = URL.createObjectURL(blob); 199 tmpa.click();//匯出後事件需要重新繫結,或者直接使用innHTML定義? 200 setTimeout(function () { 201 URL.revokeObjectURL(blob); 202 }, 100); 203 } 204 205 var str_head="<!DOCTYPE html>\n" + 206 "<html lang=\"en\">\n" + 207 "<head>\n" + 208 " <meta charset=\"UTF-8\">\n" + 209 " <title>實用版</title>\n" + 210 " <style>\n" + 211 " /*通用屬性*/\n" + 212 " body{ margin: 0; padding: 0; border: 0; text-align: center; overflow: hidden;width: 100%;\n" + 213 " height: 100%;position: fixed; font-family: verdana,arial,sans-serif; touch-action: none;\n" + 214 " -ms-touch-action: none;font-size: 12px;min-width: 600px;}\n" + 215 " ul { list-style: none; margin: 0; padding: 0;}\n" + 216 " li{ list-style: none; margin: 0; padding: 0;}\n" + 217 " ul li { float: left;}\n" + 218 " button{ cursor: pointer; height: 23px;}\n" + 219 " a:link{ text-decoration: none;}\n" + 220 " \n" + 221 " #div_top span,input,button{margin-left: 20px;float: left;}\n" + 222 " .p_h1{font-size: 24px;font-weight: bolder;background-color: darkseagreen}\n" + 223 " .p_text{font-size: 20px;font-weight: normal;text-align: left;background-color: darkseagreen}\n" + 224 "\n" + 225 " .div_section{position:relative}\n" + 226 " .p_a1{font-size: 20px;font-weight: normal;text-align: left;background-color: beige;margin-left: 60px;margin-right: 60px;\n" + 227 " ;min-height: 80px;display: block;word-break: break-all;overflow-wrap: break-word;white-space: pre-wrap}\n" + 228 " .p_a2{font-size: 20px;font-weight: normal;text-align: left;background-color: beige;left:120px}\n" + 229 " </style>\n" + 230 "</head>"; 231 </script> 232 </html>View Code
這個小工具程式碼不多,功能也不復雜,但我認為比較獨特的一點是它像單細胞生物一樣在一個檔案中實現了資訊的“保持”、“顯示”、“修改”功能,應該具備一定的可發展性,也歡迎大家在此基礎上開源更多功能。
用作例子的文章是康德的《純粹理性批判》(民國藍公武譯本)(http://book.sbkk8.com/waiguo/chuncuilixingpipan/)
感覺這本書被崇拜者們過譽了,現在更適合作為歷史和參考。
&n