22
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-09-11

quiq_top_wide.png

Quiqserch

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

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

Link

GitHub Quiqserch Issueでもプルリクでもどしどしお願いします><
chrome web store ここからダウンロードしてね! レビューも!

特徴

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

発火条件 スライダーで変更できます!

  • 最小文字数
  • デフォルトは3文字以上
  • 最大文字数
  • デフォルトは20文字未満
  • 選択継続時間
  • デフォルトは1.5秒後
  • 選択を時間内に解除すれば検索されません。
  • inputでオフ(入力フォームとか検索バー)

解説

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

│  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が読み込んでくれません。

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
  • "name": アプリ名になります
  • "author": 作成者名になります。お好きなものをどうぞ。
  • "description": 拡張機能の概要だと思います。たぶん
  • "version": 好きな番号からアップデートするごとに上げていきます。0.0.1.2 ⇒ 0.1.0.1
  • "icons": chromeの右上に表示されるアイコン
  • 大きめのpngを用意して、VSCodeの拡張機能 Image Resizer - Resize from the menu を使用して作りました。
  • もしくはfaviconジェネレーター 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
  • "permissions": chrome.storageでデータを読み書きするための権限です。

前提

chrome.storage - Google Chrome - Chrome: developer
データの保存

chrome.storage.sync.set({key: value}, function() {});

データの取得

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

browser_action

これを作ります。
screenshot.png
popup.htmlpopup.jsを読み込ませます。
popup.html全体は https://github.com/ErgoFriend/Quiqsearch/blob/master/release/popup.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
全体像です。関数はひとつずつ説明していきます。長くなりますのデ
popup.htmlではonclick=""が使えないのでインベントリスナーを使っています。

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

初期化

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で取得できますが存在しないので

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

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

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を書き換え

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が呼ばれます。

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させています。

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
DOMの書き換えは何度も行うので関数にしています。

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で取得できます。

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

GitHubへ飛ばすボタン

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

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へ文字を送ります
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では、新しいタブを開こうとするとポップアップがブロックされてしまいます。
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)として進めます。
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
    1. window.location.originでxxx.comを取得します。今回のIF条件だとwindow.location.domainではダメでした。
/youtube.com/.test(window.location.origin)
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

22
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?