Quiqsearch chrome拡張 選択するだけで検索


![quiq_top_wide.png](https://qiita-image-store.s3.amazonaws.com/0/222473/3658cc2e-d1af-72fe-d72f-9538fdd46a05.png)

# Quiqserch
chromeの拡張機能を作成しました。ググっても他のものが出てこない名前にしました :D
初めて作り、躓いたところもありましたので、そういった方へ(参考になれば)順を追って説明していきたいと思います。

選択文字を設定したn秒後に新しいタブorウィンドウで自動で検索してくれます。
だいたい、選択した後は右クリックで検索押すか上の方にドラックしてたかと思います。
自分はドラックしてました笑。

<img src="https://qiita-image-store.s3.amazonaws.com/0/222473/0e11e2cf-9ea7-b22f-fd6e-5733e9e9c3f1.gif" width="65%">

## Link
[GitHub Quiqserch](https://github.com/ErgoFriend/Quiqsearch) Issueでもプルリクでもどしどしお願いします><
[chrome web store](https://chrome.google.com/webstore/detail/quiqserch/aemnbkipehpanmmiicmofabkfllcmajj?hl=ja) ここからダウンロードしてね！ レビューも！

### 特徴
- シンプル！
- 軽い？
- 入力フォームでは動作しません (inputかどうかで判断しています。)
- 新しいタブで開く (ウィンドウは今後に期待)
- YouTubeで曲の作者やアルバム名を選択するとYouTube検索できます。

<img src="https://qiita-image-store.s3.amazonaws.com/0/222473/a5369cbd-21a0-773f-9b21-45ab51c905c0.gif" width="65%">

### 発火条件　スライダーで変更できます！
- 最小文字数
 - デフォルトは3文字以上
- 最大文字数
 - デフォルトは20文字未満
- 選択継続時間
 - デフォルトは1.5秒後
 - 選択を時間内に解除すれば検索されません。
- inputでオフ(入力フォームとか検索バー)

# 解説
実際のファイルで解説していきたいと思います。
ファイルのtree構造です。これらをフォルダに入れればデベロッパーモードでchromeに拡張機能を追加できます。

```terminal
│  manifest.json
│  popup.html
│  popup.js
│  qsearch.js
│  contentscript.js
└─images
        icon-128x128.png
        icon-16x16.png
        icon-19x19.png
        icon-48x48.png
```

最初に作るのはmanifest.jsonです。これがないとchromeが読み込んでくれません。

```json:manifest.json
{
  "manifest_version": 2,
  "name": "Quiqserch",
  "author": "ErgoFriend",
  "description": "Auto search word you selected.",
  "version": "0.1.0.0",
  "icons": {
    "16": "images/icon-16x16.png",
    "48": "images/icon-48x48.png",
    "128": "images/icon-128x128.png"
  },
  "browser_action": {
    "default_title": "Quiqserch",
    "default_icon": "images/icon-19x19.png",
    "default_popup": "popup.html"
  },
  "web_accessible_resources": ["qsearch.js"],
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["contentscript.js"]
    }
  ],
  "permissions": ["storage"]
}
```
- "manifest_version": 現行は2です [developer.chrome.com](https://developer.chrome.com/extensions/manifestVersion)
- "name": アプリ名になります
- "author": 作成者名になります。お好きなものをどうぞ。
- "description": 拡張機能の概要だと思います。たぶん
- "version": 好きな番号からアップデートするごとに上げていきます。0.0.1.2 ⇒ 0.1.0.1
- "icons": chromeの右上に表示されるアイコン
 - 大きめのpngを用意して、VSCodeの拡張機能 [Image Resizer - Resize from the menu](https://marketplace.visualstudio.com/items?itemName=lukapetrovic.image-resizer) を使用して作りました。
 - もしくはfaviconジェネレーター [realfavicongenerator.net](https://realfavicongenerator.net/)
- "browser_action": 右上の拡張機能のアイコンをクリックした時に出てくるポップアップです。
 - "default_title": ポップアップと言ってもひとつのHTMLなので
 - "default_icon": 19x19ね！
 - "default_popup": HTMLを指定します。今回は設定画面として使っています。
- "web_accessible_resources": 今回は文字の選択をトリガーにその文字列を拡張機能へ送るために、拡張機能へのアクセス許可をするファイル名を記載します
- "content_scripts": 送られた文字列を条件に合えば検索するプログラムをかきます。
 - "matches": qiitaだけで動かしたい場合は [https://qiita.com/*]() を指定します。 今回は全てのサイトを対象にしています。
 - [developer.chrome.com/extensions/content_scripts#matchAndGlob](https://developer.chrome.com/extensions/content_scripts#matchAndGlob)
- "permissions": chrome.storageでデータを読み書きするための権限です。

### 前提
[chrome.storage - Google Chrome - Chrome: developer](https://developer.chrome.com/extensions/storage)
データの保存

```javascript
chrome.storage.sync.set({key: value}, function() {});
```
データの取得

```javascript
chrome.storage.sync.get(['hoge','huga'], function(result) {
  console.log(result.hoge);
  console.log(result.huga);
});
```

## browser_action
これを作ります。
![screenshot.png](https://qiita-image-store.s3.amazonaws.com/0/222473/dedd30e3-d2a6-0725-e43d-61fb2e63e2b3.png)
```popup.html```に```popup.js```を読み込ませます。
```popup.html```全体は [https://github.com/ErgoFriend/Quiqsearch/blob/master/release/popup.html](https://github.com/ErgoFriend/Quiqsearch/blob/master/release/popup.html)

```html:popup.html
<head>
     <!-- 省略 -->
    <script src="popup.js"></script>
</head>
<body>
    <div class="slidecontainer head">
            <label class="switch">
            <input type="checkbox" id="status">
            <span class="slider round"></span>
        </label>
    </div>
    <div class="slidecontainer">
        <div class="title">
            min length:
            <div id="min_text"></div>
        </div>
        <div class="container">
            <input type="range" id="min_value" class="input-range" min="0" max="100" value="0">
        </div>
    </div>
    <!-- 省略 -->
</body>
```

読み込んだ```popup.js```では設定のストレージへの保存と```popup.html```への反映を行っています。

```popup.js```全体は [https://github.com/ErgoFriend/Quiqsearch/blob/master/release/popup.js](https://github.com/ErgoFriend/Quiqsearch/blob/master/release/popup.js)
全体像です。関数はひとつずつ説明していきます。長くなりますのデ
```popup.html```では```onclick=""```が使えないのでインベントリスナーを使っています。

1. 流れとしては、ポップアップを開くとストレージから現在の設定を読み込み、初めて使用する場合は```init()```でデフォルトの設定を書き込んでから読み込みます。
 1. 一番最初に、書き換える要素を変数に入れておきます。
2. 読み込んだ後、HTMLの各idへ対応した値でHTMLの```value="0"```を書き換えていきます。
 1. その後にイベントリスナーで監視をしない場合、```value="0"```がストレージへ書き込まれてしまいました。
3. あとは、UIから値が変更されたらイベントリスナーで感知して変更された値をストレージへ書き込んでいきます。
4. スライダーの場合、それだけだとスライダーの位置が変わっても左にある値が変わらないので
```chrome.storage.onChanged.addListener```でストレージの値を監視して、変更されたらその値でDOMを書き換えていきます。

```javascript:popup.js
let docs = function() {};

let setValue = function(value, out_text, out_value) {};

let init = function() {};

// DOM is changed, save to storage.
let eventListener = function(event) {};

document.addEventListener("DOMContentLoaded", () => {
  //DOMを書き換えるために要素を取得
  let xxxxx= document.getElementById("xxxxx");

  // init settings data from storage.
  init() //初回起動か確認
    .then(function(result) {
      //初回起動か確認
    })
    .then(function() {
      //ポップアップ起動時にストレージのデータからDOMを書き換え
    })
    .catch(function(error) {
      console.log(error);
    });

  // changing storage value from input acction
  // DOMの監視
  xxxxxx.addEventListener("input", eventListener);

  // changing DOM value from listen changed storage
  chrome.storage.onChanged.addListener(function(changes, namespace) {
  });

  // go to GitHub bage
  document.querySelector("button").addEventListener("click", docs);
});
```
### 初期化

```javascript:popup.js
let init = function() {
  return new Promise((resolve, reject) => {
    chrome.storage.sync.get(["initialed"], function(value) {
      if (!value.initialed) {
        // first time use this extention.
        // set dafault value to storage
        chrome.storage.sync.set({ min: Number(3) }, function() {});
        chrome.storage.sync.set({ max: Number(20) }, function() {});
        chrome.storage.sync.set({ time: Number(1.5) }, function() {});
        chrome.storage.sync.set({ status: true }, function() {});
        chrome.storage.sync.set({ inputtextarea: true }, function() {});
        chrome.storage.sync.set({ youtube: false }, function() {});
        console.log("first init done!");
        resolve("false");
      } else {
        console.log("first init had been finished.");
        resolve("true");
      }
    });
  });
};

document.addEventListener("DOMContentLoaded", () => {
  // init settings data from storage.
  init()
    .then(function(result) {
      return new Promise((resolve, reject) => {
        if (result == "false") {
          chrome.storage.sync.set({ initialed: true }, function() {});
          console.log("initialed");
        }
        resolve();
      });
    })
    .then(function() {
      // When open popup
      console.log("init set");
      return new Promise((resolve, reject) => {
        chrome.storage.sync.get(
          ["min", "max", "time", "status", "inputtextarea", "youtube"],
          function(value) {
            setValue(value.min, target1, elem1);
            setValue(value.max, target2, elem2);
            setValue(value.time, target3, elem3);
            document.getElementById("status").checked = value.status;
            document.getElementById("onoff").innerHTML = value.status
              ? "ON"
              : "OFF";
            document.getElementById("inputtextarea").checked =
              value.inputtextarea;
            document.getElementById("youtube").checked = value.youtube;
          }
        );
      });
    })
    .catch(function(error) {
      console.log(error);
    });
});
```
#### 初回起動か確認
拡張機能をインストールして、初めてポップアップを開いたときは当然ながらデータは存在しません。
そこで、無いのを前提にストレージへ```initialed```を取得しに行きます。
データは```value.initialed```で取得できますが存在しないので

```javascript:popup.js
if (!value.initialed) {
  //初期化
  //ストレージへ初期設定を保存
  resolve("false");
} else {
  //初期化済み
  resolve("true");
}
```

```!value.initialed```で ```true```となり初期化、つまりストレージへ初期設定が保存されます。
```init()```はPromiseを返すので、```resolve()```に初期化済みだったか```true/false```を入れています。
わざわざ```resolve()```で返す必要もないと思いますが、Promiseの勉強になったので良しとします。
```.then()```で受け取った```true/false```で```false```の場合に、ストレージへ```{ initialed: true }```を保存しています。

```javascript:popup.js
init()
  .then(function(result) {
    return new Promise((resolve, reject) => {
      if (result == "false") {
        chrome.storage.sync.set({ initialed: true }, function() {});
        console.log("initialed");
      }
      resolve();
    });
  })
```
ポップアップの表示はこの後に、先ほど保存された/もしくは二度目以降の使用で保存されてあったデータでDOMを書き換えることで完了となります。
#### ポップアップ起動時にストレージのデータからDOMを書き換え

```javascript:popup.js
init()
  .then(function(result) {
  //今説明したやつ
  //初期化されてなかったら初期化したことを書き込む
  })
  .then(function() {
    // When open popup
    //ストレージにあるデータをDOMへ書き込みます
    console.log("init set");
    return new Promise((resolve, reject) => {
      chrome.storage.sync.get(
        ["min", "max", "time", "status", "inputtextarea", "youtube"],
        function(value) {
          setValue(value.min, target1, elem1);
          setValue(value.max, target2, elem2);
          setValue(value.time, target3, elem3);
          document.getElementById("status").checked = value.status;
          document.getElementById("onoff").innerHTML = value.status
            ? "ON"
            : "OFF";
          document.getElementById("inputtextarea").checked =
            value.inputtextarea;
          document.getElementById("youtube").checked = value.youtube;
        }
      );
    });
  })
```

### addEventListenerでデータの保存
DOMを監視するイベントリスナーは初期化されてから始まります。
DOMの要素が変更されると、イベントリスナーに登録した関数```eventListener```が呼ばれます。

```javascript:popup.js
// DOM is changed, save to storage.
let eventListener = function(event) {};

document.addEventListener("DOMContentLoaded", () => {
  let target1 = document.getElementById("min_text");
  let target2 = document.getElementById("max_text");
  let target3 = document.getElementById("time_text");
  let elem1 = document.getElementById("min_value");
  let elem2 = document.getElementById("max_value");
  let elem3 = document.getElementById("time_value");
  let elem4 = document.getElementById("status");
  let eleminput = document.getElementById("inputtextarea");
  let elemyoutube = document.getElementById("youtube");

  // init settings data from storage.
  init...
  // changing storage value from input acction
  elem1.addEventListener("input", eventListener);
  elem2.addEventListener("input", eventListener);
  elem3.addEventListener("input", eventListener);
  elem4.addEventListener("input", eventListener);
  eleminput.addEventListener("input", eventListener);
  elemyoutube.addEventListener("input", eventListener);
});
```
#### イベントリスナー
変更された値は、今回の場合
```<input type="range">```なら```event.currentTarget.value```
```<input type="checkbox">```なら```event.currentTarget.checked```で取得できます。
どの要素が変更されたかは```event.currentTarget.id```で取得できるので```switch case```させています。

```javascript:popup.js
// DOM is changed, save to storage.
let eventListener = function(event) {
  const num = event.currentTarget.value;
  switch (event.currentTarget.id) {
    case "min_value":
      chrome.storage.sync.set({ min: Number(num) }, function() {});
      break;
    case "max_value":
      chrome.storage.sync.set({ max: Number(num) }, function() {});
      break;
    case "time_value":
      chrome.storage.sync.set({ time: Number(num) }, function() {});
      break;
    case "status":
      chrome.storage.sync.set(
        { status: event.currentTarget.checked },
        function() {}
      );
      console.log("status :", event.currentTarget.checked);
      break;
    case "inputtextarea":
      chrome.storage.sync.set(
        { inputtextarea: event.currentTarget.checked },
        function() {}
      );
      console.log("inputtextarea :", event.currentTarget.checked);
      break;
    case "youtube":
      chrome.storage.sync.set(
        { youtube: event.currentTarget.checked },
        function() {}
      );
      console.log("youtube :", event.currentTarget.checked);
      break;
    default:
      console.log("Invaild eventListener case error");
  }
};
```

### chrome.storage.onChanged.addListenerでDOMの書き換え

```chrome.storage.onChanged.addListener```はストレージの監視を行ってくれます。[chrome.storage - Google Chrome - Chrome: developer](https://developer.chrome.com/extensions/storage)
DOMの書き換えは何度も行うので関数にしています。

```javascript:popup.js
//DOMの書き換え
let setValue = function(value, out_text, out_value) {
  console.log("set value:", value, "change", out_text.id, "&", out_value.id);
  out_value.value = value;
  out_text.innerHTML = value;
};

document.addEventListener("DOMContentLoaded", () => {
  //DOMを書き換えるために要素を取得
  let xxxxx= document.getElementById("xxxxx");

  // changing DOM value from listen changed storage
  chrome.storage.onChanged.addListener(function(changes, namespace) {
    for (key in changes) {
     //DOMの書き換え
      let storageChange = changes[key];
      let num = storageChange.newValue;
      switch (key) {
        case "min":
          setValue(num, target1, elem1);
          break;
        case "max":
          setValue(num, target2, elem2);
          break;
        case "time":
          setValue(num, target3, elem3);
          break;
        case "status":
          document.getElementById("status").checked = storageChange.newValue;
          document.getElementById("onoff").innerHTML = storageChange.newValue
            ? "ON"
            : "OFF";
          break;
        case "inputtextarea":
          break;
        case "youtube":
          break;
        default:
          console.log("Invaild onChanged case error");
      }
    }
  });
});
```
#### chrome.storage.onChanged.addListener
変更されたものは```changes[key]```、変更された値は```changes[key].newValue```で取得できます。

```javascript
chrome.storage.onChanged.addListener(function(changes, namespace) {
  for (key in changes) {
    let storageChange = changes[key];
    console.log(storageChange.newValue);
    }
  }
});
```


### GitHubへ飛ばすボタン
イベントリスナーで```button```を監視し、押されたらGitHubのページを開いています。

```javascript:popup.js
let docs = function() {
  window.open("https://github.com/ErgoFriend/Quiqsearch", "_blank");
};

document.addEventListener("DOMContentLoaded", () => {
  // go to GitHub bage
  document.querySelector("button").addEventListener("click", docs);
});

```

## content_scripts

これは今回の拡張機能を拡張機能たらしめている？部分です。
構成としては、二つのファイルがあります。

- ```qsearch.js``` : 選択した文字を拡張機能に送る役割
- ```contentscript.js``` : 受け取った文字で検索する役割、```qsearch.js```を閲覧しているサイトへ書き込む役割

まず```qsearch.js```を見ちゃいます。短いですねーイベントリスナーで文字の選択を監視します。
選択されたら、

1. ```const str = window.getSelection().toString();``` : 文字の取得をします
2. ```window.postMessage({ keyword: str }, "*");``` : ```contentscript.js```へ文字を送ります

```javascript:qsearch.js
document.addEventListener("selectionchange", function(event) {
  const str = window.getSelection().toString();
  window.postMessage({ keyword: str }, "*");
});
```

#### contentscript.js
このファイルが必要な理由としては、

- ストレージに保存した変数を条件としたトリガーを持っていること。閲覧ページへ書き込んだ```qsearch.js```では```chrome.storage.sync```へアクセスできません。
- 新しいタブを開く。閲覧ページへ書き込んだ```qsearch.js```では、新しいタブを開こうとするとポップアップがブロックされてしまいます。

```javascript:contentscript.js
//閲覧ページへqsearch.jsを書き込みます
var s = document.createElement("script");
s.src = chrome.extension.getURL("qsearch.js");
(document.head || document.documentElement).appendChild(s);
s.parentNode.removeChild(s);

//送られた文字を受け取る
window.addEventListener(
  "message",
  function receive(event) {
    if (event.data.keyword) {
      const str = event.data.keyword;
      const leng = str.length;
      chrome.storage.sync.get(
        ["min", "max", "time", "status", "inputtextarea", "youtube"],
        function(result) {
          if (result.status) {
            if (!result.inputtextarea || document.activeElement.nodeName != "INPUT" ) {
              console.log(document.activeElement.nodeName);
              if (result.min <= leng && leng <= result.max) {
                setTimeout(function() {
                  if (str == window.getSelection().toString()) {
                    let search_url = "";
                    if (/youtube.com/.test(window.location.origin)) {
                      console.log("youtube search: true");
                      search_url = result.youtube ? "https://www.youtube.com/results?search_query=" : "https://www.google.com/search?q=";
                    } else {
                      search_url = "https://www.google.com/search?q=";
                    }
                    window.open(search_url + encodeURI(str), "_blank");
                  }
                }, result.time * 1000); //result.time秒後
              }
            }
          }
        }
      );
    }
  },
  false
);
```
##### ifがいくつも出てきているので順番に説明していきます。 実行された場合(```true```)として進めます。
```javascript
1. if(){
  2. if(){
     ...
  }
}
```
って感じです。

1. ```if (event.data.keyword) {}``` : 受け取ったデータに```keyword```が入っていたら実行します。
2. ```chrome.storage.sync.get``` : 設定を読み込みます。
3. ```if (result.status) {}``` : ```result.status```にはポップアップ右上のこの拡張機能のON(```true```)/OFF(```false```)情報を持たせています。
4. ```if (!result.inputtextarea || document.activeElement.nodeName != "INPUT" ) {}``` : 入力フォームでの動作条件です。
    1. ```result.inputtextarea```は 入力フォームで動作させるかどうかの情報を持っています。```true```が動作させないことを示します。
    2. ```document.activeElement.nodeName```は選択している要素名を取得します。入力フォームのとき```INPUT```を返します。
    3. 4.1をα、4.2をβとすると ( α AND β )のときに動作させたくないので、ド・モルガンの法則で( !α OR !β )になります。
5. ```if (result.min <= leng && leng <= result.max) {}``` : ポップアップから設定できる検索させる文字列の長さの条件です。
6. ```setTimeout(function() { if (str == window.getSelection().toString()) {} }, result.time * 1000)```
    1. ```setTimeout(function() {}, result.time * 1000)```は、ポップアップで設定したwaiting sec: N秒後に中身が実行されます。
    2. ```if (str == window.getSelection().toString()) {}``` は選択した文字が、N秒後も同じか判断します。( N秒間選択したままかどうか )
7. ```if (/youtube.com/.test(window.location.origin)) {} else {}``` : YouTubeにいるか判断します。RegExpです。[RegExp - JavaScript - MDN - Mozilla](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/RegExp)
    1. ```window.location.origin```でxxx.comを取得します。今回のIF条件だと```window.location.domain```ではダメでした。

##### /youtube.com/.test(window.location.origin)

```javascript
let search_url = "";
if (/youtube.com/.test(window.location.origin)) {
 search_url = result.youtube ? "https://www.youtube.com/results?search_query=" : "https://www.google.com/search?q=";
} else {
 search_url = "https://www.google.com/search?q=";
}
window.open(search_url + encodeURI(str), "_blank");
```

- ```if``` YouTubeにいる(```true```)とき
 - ```result.youtube```はYouTubeない検索のON(```true```)/OFF(```false```)情報を持ちます。
 - これがON(```true```)のときは、```search_url```に```"https://www.youtube.com/results?search_query="```が代入されます。
 - これがOFF(```false```)のときは、```search_url```に```"https://www.google.com/search?q="```が代入されます。
- ```esle``` YouTubeにいない(```false```)とき
 - ```search_url```に```"https://www.google.com/search?q="```が代入されます。

振り返ると、```else```使わなくても```AND```で良さそうに思えますが、三項演算子がきれいに使えたので自分的にはOKです。

# 参考文献　お世話になったところ
http://waitingphoenix.com/how-to-make-your-chrome-extension-access-webpage/
https://kuroeveryday.blogspot.com/2015/06/ChromeExtensionssendMessage.html
https://developer.chrome.com/extensions/storage