設計
Chrome Extension ( Chrome 拡張 ) で 理想のTodoを作成しました。
理想の拡張はなかなかないもので、プログラマなら拡張は自分で作れると楽しいな、と思います。
このTodoは、月一しかやらないが、月一には絶対に忘れずにやりたいリスト、を作成するものです。
done/undoneのチェックが行えて、迫ってくると色で教えてくれ、過ぎていると色で教えてくれます。
そして月が変わると自動的にリセットされます。
具体的には以下のような機能を盛り込みます。
・毎月行うことを日付別に記録
・日付でソートされる
・done/undoneが記録でき、ソートされる( done すると下に移動 )
・近くなったり、超過したりすると色とソート順が変化 ( 超過 -> 近い -> 普通 の順番になる )
・月を跨ぐと完了がリセットされる。
この拡張をちょっと作り直すだけで、様々なTodoや、毎日リセットされる毎日やることTodoなども出来ますので、作成しています。
作成内容を順番に記述していきます。
バージョン3とハマりどころ
Chrome 拡張は現在 version 3 になっています。マニフェストをversion3で書く必要があります。
version2の頃に出来たいくつかのことがどうやら出来なくなっています。
Evalが使えない
・eval
・new Function
これらは要するに、文字列からプログラムでプログラムを書き換える機能です。
write という 関数に対して、後で文字列から write という関数を上書き、などということができます。
vue.js が使えるとよかったのですが、vue.jsはどうやらevalを用いているようです。
sandboxにすれば使えましたが、そうするとstorage API にアクセス出来ず、記録機能が使えませんので、認証して別途サーバーに記録しなければならないでしょう。
chrome.storageを使って記録したいので、sandboxは使えません。
content_script_policy を設定してもevalが使えるようにならず、stackoverflowを調べると「eval使うなってことだよ」と書かれているので、そもそもvue.jsをやめました。
なので、このリストのリフレッシュを自前で書いていますが、この程度の大きさでしたら、全く遅延しません。
必要最低限、動くことが重要です。
外部リソースが使えない
簡単にいうと、scriptタグによって外部、例えばcdnなどからライブラリを呼べません。
よって全てDLして同梱する必要があります。
マニフェストファイルを作成する
ということでかなり不自由が増えている気がしますが、マニフェストファイルを書いてみます。
1 2 3 4 5 6 7 8 9 10 |
{ "name": "月一の君を忘れない", "description":"月一回、毎回やる作業を忘れないための拡張です。", "manifest_version": 3, "version": "0.1", "permissions": ["storage"], "action": { "default_popup": "popup.html" } } |
ここでdefault_popupを設定します。
そしてpopup.htmlを作成します。
この二つのファイルだけで、拡張は最低限動きます。
Chrome左上の拡張一覧からピン留めすると、アイコンが出現し、それをクリックするとpopup.htmlが呼ばれるのです。
popup.html を作成する
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>Loops</title> <link rel="stylesheet" href="popup.css"> <script type="text/javascript" src="jquery.js"></script> <script type="text/javascript" src="moment.js"></script> <script type="text/javascript" src="underscore.js"></script> <script type="text/javascript" src="app.js"></script> </head> <body> <span id="today"></span><br> <div style="text-align: right; padding-right: 20px;">実行日 : <select id="theDate"> <option value="1">1</option> <option value="2">2</option> <option value="3">3</option> <option value="4">4</option> <option value="5">5</option> <option value="6">6</option> <option value="7">7</option> <option value="8">8</option> <option value="9">9</option> <option value="10">10</option> <option value="11">11</option> <option value="12">12</option> <option value="13">13</option> <option value="14">14</option> <option value="15">15</option> <option value="16">16</option> <option value="17">17</option> <option value="18">18</option> <option value="19">19</option> <option value="20">20</option> <option value="21">21</option> <option value="22">22</option> <option value="23">23</option> <option value="24">24</option> <option value="25">25</option> <option value="26">26</option> <option value="27">27</option> <option value="28">28</option> <option value="29">29</option> <option value="30">30</option> <option value="31">31</option> </select>日</div> <textarea id="theContent"></textarea> <br><button id="enter-button">セット</button> <div id="app"> </div> </body> </html> |
次に popup.css、app.js を作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
body{ min-width: 300px; min-height: 400px; } #title{ display: inline-block; background-color: black; color: white; font-weight: bold; border-radius: 3px; padding: 2px; } #app{ width: 100%; } #app div{ width: 90%; padding: 5px; margin-bottom: 3px; border: 1px solid black; text-align: left; position: relative; } #app div.over{ border: 2px solid red; } #app div.near{ border: 2px solid #8b008b; } #app div.done{ border: 0px solid gray; background-color: gray; } #app div button.done-button{ position: absolute; top: 1px; right: 40px; } #app div button.undone-button{ position: absolute; top: 1px; right: 40px; } #app div button.del-button{ position: absolute; top: 1px; right: 1px; } #theContent{ width: 90%; min-height: 200px; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 |
items = [] app = null tag = null theDate = 1 refreshList = function(){ app.empty() // sort let itemsOver = [] let itemsNear = [] let itemsDone = [] let itemsNotDone = [] _.each( items, function(v){ if( v.done ){ itemsDone.push(v) }else{ console.log(theDate > parseInt(v.date)) if( theDate > parseInt(v.date) ){ v.status = "over" itemsOver.push(v) }else if( theDate > parseInt(v.date) - 3 ){ v.status = "near" itemsNear.push(v) }else{ v.status = "notdone" itemsNotDone.push(v) } } }) itemsOver = _.sortBy(itemsOver,"date") itemsNear = _.sortBy(itemsNear,"date") itemsDone = _.sortBy(itemsDone,"date") itemsNotDone = _.sortBy(itemsNotDone,"date") _items = [] _.each( itemsOver, function(v){ _items.push(v) }) _.each( itemsNear, function(v){ _items.push(v) }) _.each( itemsNotDone, function(v){ _items.push(v) }) _.each( itemsDone, function(v){ _items.push(v) }) items = _items _.each( items, function(v,k){ if( v.done == true ){ app.append("<div class='done'>"+v.date+"日 " +"<button class='undone-button' uid='"+k+"'>undone</button>" +"<button class='del-button' uid='"+k+"'>del</button><br>"+v.val+"</div>") }else{ if( v.status == "over"){ app.append("<div class='over'>"+v.date+"日 " +"<button class='done-button' uid='"+k+"'>done</button>" +"<button class='del-button' uid='"+k+"'>del</button><br>"+v.val+"</div>") }else if( v.status == "near" ){ app.append("<div class='near'>"+v.date+"日 " +"<button class='done-button' uid='"+k+"'>done</button>" +"<button class='del-button' uid='"+k+"'>del</button><br>"+v.val+"</div>") }else{ app.append("<div>"+v.date+"日 " +"<button class='done-button' uid='"+k+"'>done</button>" +"<button class='del-button' uid='"+k+"'>del</button><br>"+v.val+"</div>") } } } ) $(".del-button").off("click") $(".del-button").on("click",function(e){ let uid = $(e.target).attr("uid") let _items = [] _.each( items, function(v,k){ if( k != uid ){ _items.push(v) } }) items = _items chrome.storage.local.set({myItems:items}) refreshList() }) $(".done-button").off("click") $(".done-button").on("click",function(e){ let uid = $(e.target).attr("uid") _.each( items, function(v,k){ if( k == uid ){ v.done = true } }) chrome.storage.local.set({myItems:items}) refreshList() }) $(".undone-button").off("click") $(".undone-button").on("click",function(e){ let uid = $(e.target).attr("uid") _.each( items, function(v,k){ if( k == uid ){ v.done = false } }) chrome.storage.local.set({myItems:items}) refreshList() }) } window.onload = function(){ app = $("#app") let theMonth = moment().format("YYYY年MM月DD日") theDate = parseInt(moment().format("D")) $("#today").text(theMonth) tag = moment().format("@@YYYYMM@@") $("#enter-button").on("click", function(){ let date = $("#theDate").val() let val = $("#theContent").val().replace(/\n/g, "<br>") items.push({date: parseInt(date), val:val, done: false}) chrome.storage.local.set({myItems:items}) refreshList() }) chrome.storage.local.get(null, function(data){ if( data.myItems ){ items = data.myItems }else{ items = [] } if( data.myTag ){ if( data.myTag != tag ){ alert("新しい月が始まり、完了がリセットされました。") _.each( items, function(v){ v.done = false }) } chrome.storage.local.set({myTag:tag,myItems:items}) }else{ chrome.storage.local.set({myTag:tag}) } loaded = true refreshList() }) } |
moment.js、 underscore.js、jquery.js はそれぞれのライブラリからDLし、同じフォルダに置く事で、読み取れます。
Chrome で読み込む
Google Chromeで 「chrome://extensions/」にアクセスすると拡張の一覧が見えます。
上のメニューバーの右側にある「デベロッパーモード」をONにすると、
「パッケージ化されていない拡張機能を読み込む」ボタンが出てきます。
ここから、該当のマニフェストが置いてあるフォルダを指定します。
フォルダ構成は
extension-folder
┣ manifest.json
┣ popup.html
┣ app.js
┣ popup.css
┣ jquery.js
┣ underscore.js
┗ moment.js
となっていると思いますが、その一番上の extension-folder を指定します。
完成です!!
感動的です。これは最高です!w