Googleカレンダーのブックマークレットを作る過程を淡々と記録する

### この記事の目的

Googleカレンダーを便利に使うブックマークレットを作る際の中間生成物をじょじょに上げていく。

### 作りたいもの

- Googleカレンダーで予定を仮押さえした情報をプレーンテキストで取り出したい（日程調整によく使うやつ）
- それをクップボードにコピーしたい
- Gooleカレンダーには検索結果一覧画面があるのでそこから情報を取り出すブックマークレットを作りたい
- 過去の予定がヒットすることもあるので、過去の予定は無視したい

### まず検索結果一覧のクラスを特定

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53489/74131190-3eda-9387-c8d3-c06cd046e684.png)

この画面からこの一覧を取得するCSSセレクタを特定

普通だったら右クリックで要素を特定するけどこの画面は右クリックが機能として奪われてる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53489/239ab23a-e302-96df-723f-e61a6c2d49da.png)


こういうときにはデベロッパーツールの検索機能で対象文字列で探す
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53489/6c4bb4c7-efaf-92b2-75cf-4af863af8dc2.png)

予定一覧のありかがわかったのであとはそこから徐々に上にたどりながら一覧を取得するdivを探し当てる。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53489/7b6fab0e-8230-b27d-5a29-051f048063b8.png)

どうも`div[role=rowgroup]`で行けそうだとも思うが、実際に`div[role=rowgroup]`で検索してみると、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53489/2cdaf7c3-31b5-9332-9c6c-f5fb02ed580d.png)

左の月間カレンダーにもヒットしています。もうちょっと特徴はないか。

`div[role=rowgroup] div[role=row]:has(div[role=gridcell])` までCSSセレクタを絞り込むことで、各行を特定できた。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53489/3f97a2ba-2a98-3d0e-ad8f-7f600f1d8603.png)

右下に`2 of 3`と表示されていることから、3件ヒットしていることが分かる。

### JS化する

ここまでくれば、あとは各行の要素を個別に取り出せばいい。JSをゴリゴリ書くんだけど、まずはconsoleでデバッグしてみる。

```js
$$('div[role=rowgroup] div[role=row]:has(div[role=gridcell])').forEach(
  row => {
    console.log("---");
    console.log(row.innerText);
  }
)
```

これをDevConsoleに貼り付け。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53489/616a6462-0f2c-69c2-9e2d-6778e3391e33.png)

3行でてるし、画面上に出てるテキストがデバッグできてるから期待通り。

あとは各要素を取り出せばいい。このブックマークレットで欲しいのは以下の要素。

- 月日
- 曜日
- 時刻のfrom-to
- 年月日（過去の予定を取り除くためのフルの日付）


```js
$$('div[role=rowgroup] div[role=row]:has(div[role=gridcell])').forEach(
  row => {
    console.log("---");
    console.log(row.querySelector('[aria-label]').getAttributeNode("aria-label").value);
    console.log(row.querySelectorAll('[role=gridcell]')[1].innerText);
    console.log(row.querySelector('[role=gridcell] [role=button]').getAttributeNode("aria-label").value);
  }
)
```

デバッグ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53489/a447a4ed-d3a3-e1df-f1ea-9d1f0286c1ee.png)

期待通り。

ここまできたら次は、メールに貼り付ける文字列を作る

```js
let lines = []
$$('div[role=rowgroup] div[role=row]:has(div[role=gridcell])').forEach(
  row => {
    console.log("---");
    let mda = row.querySelector('[aria-label]').getAttributeNode("aria-label").value
    console.log(mda);
    let time_range = row.querySelectorAll('[role=gridcell]')[1].innerText 
    lines.push(mda + " " + time_range);
    console.log(row.querySelector('[role=gridcell] [role=button]').getAttributeNode("aria-label").value);
  }
)
console.log(lines);
```

実行結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53489/422937d3-c5fc-2f57-2337-fb649bed8767.png)

期待通り

### クリップボードにコピーする機能を調べる

次にクリップボードにコピーしてみよう。
クリップボードアクセスはセキュリティ制限があれこれあって何度もアーキテクチャが変わってる。
ここの仕様の安定性は期待せずにさがしてみると、クリップボードAPIというのがあるらしい。

まずは単体で使ってみよう。

まずは手元のChromeで使えるかどうかを確認。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53489/a961e012-4722-fabd-e8a7-b9a5a3a763fa.png)

クリップボードオブジェクトが帰ってきた。何か設定をいじることなく使えるみたい。

クリップボードにコピーするメソッドを使ってみる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53489/befc9038-9d5b-f383-4287-fb2e13a1bba9.png)

ドキュメントにフォーカスが無いよというエラーが出てきた。ドキュメントにフォーカスがないとダメなのね。DevTools上にフォーカスがあるからそりゃぁそうか。このあたりはDevToolsではテストできなくなる。制約があるなら仕方ない。

じゃぁ本物のブックマークレットではクリップボードアクセスできるのかどうかをテスト。

まずは、ブックマークレットの雛形を用意。

```js
javascript:(function(){alert('ya')})();
```

これをブックマークにセット。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53489/f426ea1d-abf5-4da5-8cb0-7a3710f528d9.png)

クリックして実行

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53489/7c33b33d-71c4-28d5-c06e-f09bb042ca08.png)

うごいたうごいた。

次にブックマークレットの中でクリップボードオブジェクトが使えるか試す。


```js
javascript:(function(){alert(navigator.clipboard)})();
```

このJSをクリップボードにセット

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53489/0c2d8a6d-bcc2-ad04-956f-017b5f31e55d.png)

テスト実行

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53489/344cd876-90d1-09cd-50e5-baecb1f4a6a3.png)

クリップボードオブジェクトは使える。

次にwritetextが使えるか。
(以降、ブックマークレットの編集画面はすっ飛ばします）

```js
javascript:(function(){navigator.clipboard.writeText("<empty clipboard>")})();
```

これをブックマークレットとして実行。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53489/26e8e4cc-dade-8d3b-d8fa-59134e4eacd8.png)

はいった。目標達成。

じゃぁってことで、元のGoogleカレンダーの取得のロジックと組み合わせたい。

### ブックマークレットの制約を調べる

そういや、ブックマークレットって複数行のJSが入るとどうなるんだろう。
試しにミニマムのテストをやってみる。

```js
javascript:(
  function(){
    let lines = [];
    lines.push('aa');
    lines.push('bb');
    navigator.clipboard.writeText(lines.join("\n"));
  }
)();
```
改行ありのままブックマークレットの編集でペースト

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53489/e0f9d431-6330-b7a2-f0f0-a5168c28ba5c.png)

改行込でもペーストできた。

そのまま実行してみると。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53489/ac9583f7-bbe5-1a0d-168a-99ce76aed742.png)

ちゃんと実行できた。

### 徐々にDevTools上で動いていたロジックを移植。

DevTools上では動いていたけど、ブックマークにペーストしたとたんに動かなくなるポイントがある。

- `$$`はDevTools上でしか動かないので、document.querySelectorAllに変更(すっかり忘れてた)
- DevTools上では改行があるので動いていたけど、ブックマークレットだと一行になるので、セミコロン忘れると動かなくなるのでその辺を修正（普段JS書き慣れてないと雑になりがち）

それでできあがりがこれ

```js
javascript:(
  function(){
    let lines = [];
    document.querySelectorAll('div[role=rowgroup] div[role=row]:has(div[role=gridcell])').forEach(
      row => {
        let mda = row.querySelector('[aria-label]').getAttributeNode("aria-label").value;
        let time_range = row.querySelectorAll('[role=gridcell]')[1].innerText;
        lines.push(mda + " " + time_range);
        console.log(row.querySelector('[role=gridcell] [role=button]').getAttributeNode("aria-label").value);
      }
    );
    console.log(lines);
    navigator.clipboard.writeText(lines.join("\n"));
  }
)();
```

動かしてみると

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53489/e75ef342-f6fe-6b47-91ef-64c19a914ce4.png)

予定の一覧がクリップボードに入った。

### 非優先機能を見極める

最初にやりたかった「過去の予定を排除」というロジックを入れたかったけど、そもそも予定の仮押さえは「〇〇会議仮」みたいに仮押さえということが分かるようにするから、過去の仮押さえ予定がヒットしすることが稀だよなということを思った。あとここまで完成すると熱意が減ってきた。

試しに過去の予定がヒットした状態でブックマークレットを実行してみた。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53489/5eaad3ce-3b08-a53c-e841-96a4507340d3.png)

この画面でブックマークレットを実行すると、こう

```
2006年 4月 1日 (土曜日) 終日
2007年 4月 2日 (月曜日) 13:00～18:00
2009年 4月 2日 (木曜日) 12:00～21:30
2010年 4月 3日 (土曜日) 14:00～17:00
```

万が一去年の予定が混じっても、去年の予定だということも分かるからまぁいいか。過去の予定排除機能はこのブックマークレットには機能としては入れない。

### 完成形

ということで完成形のブックマークレットはこれ。

```js
javascript:(
  function(){
    let lines = [];
    document.querySelectorAll('div[role=rowgroup] div[role=row]:has(div[role=gridcell])').forEach(
      row => {
        let mda = row.querySelector('[aria-label]').getAttributeNode("aria-label").value;
        let time_range = row.querySelectorAll('[role=gridcell]')[1].innerText;
        lines.push(mda + " " + time_range);
      }
    );
    navigator.clipboard.writeText(lines.join("\n"));
  }
)();
```

ざっくり2時間でできる割にはまぁまぁ有用なツールができた。もっと早くに作っておけばよかった。