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_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
これを作ります。
popup.html
にpopup.js
を読み込ませます。
popup.html
全体は https://github.com/ErgoFriend/Quiqsearch/blob/master/release/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=""
が使えないのでインベントリスナーを使っています。
- 流れとしては、ポップアップを開くとストレージから現在の設定を読み込み、初めて使用する場合は
init()
でデフォルトの設定を書き込んでから読み込みます。 - 一番最初に、書き換える要素を変数に入れておきます。
- 読み込んだ後、HTMLの各idへ対応した値でHTMLの
value="0"
を書き換えていきます。 - その後にイベントリスナーで監視をしない場合、
value="0"
がストレージへ書き込まれてしまいました。 - あとは、UIから値が変更されたらイベントリスナーで感知して変更された値をストレージへ書き込んでいきます。
- スライダーの場合、それだけだとスライダーの位置が変わっても左にある値が変わらないので
chrome.storage.onChanged.addListener
でストレージの値を監視して、変更されたらその値でDOMを書き換えていきます。
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);
});
初期化
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
で取得できますが存在しないので
if (!value.initialed) {
//初期化
//ストレージへ初期設定を保存
resolve("false");
} else {
//初期化済み
resolve("true");
}
!value.initialed
で true
となり初期化、つまりストレージへ初期設定が保存されます。
init()
はPromiseを返すので、resolve()
に初期化済みだったかtrue/false
を入れています。
わざわざresolve()
で返す必要もないと思いますが、Promiseの勉強になったので良しとします。
.then()
で受け取ったtrue/false
でfalse
の場合に、ストレージへ{ initialed: true }
を保存しています。
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を書き換え
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
が呼ばれます。
// 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
させています。
// 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の書き換えは何度も行うので関数にしています。
//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のページを開いています。
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
を見ちゃいます。短いですねーイベントリスナーで文字の選択を監視します。
選択されたら、
-
const str = window.getSelection().toString();
: 文字の取得をします -
window.postMessage({ keyword: str }, "*");
:contentscript.js
へ文字を送ります
document.addEventListener("selectionchange", function(event) {
const str = window.getSelection().toString();
window.postMessage({ keyword: str }, "*");
});
contentscript.js
このファイルが必要な理由としては、
- ストレージに保存した変数を条件としたトリガーを持っていること。閲覧ページへ書き込んだ
qsearch.js
ではchrome.storage.sync
へアクセスできません。 - 新しいタブを開く。閲覧ページへ書き込んだ
qsearch.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(){
...
}
}
って感じです。
-
if (event.data.keyword) {}
: 受け取ったデータにkeyword
が入っていたら実行します。 -
chrome.storage.sync.get
: 設定を読み込みます。 -
if (result.status) {}
:result.status
にはポップアップ右上のこの拡張機能のON(true
)/OFF(false
)情報を持たせています。 -
if (!result.inputtextarea || document.activeElement.nodeName != "INPUT" ) {}
: 入力フォームでの動作条件です。-
result.inputtextarea
は 入力フォームで動作させるかどうかの情報を持っています。true
が動作させないことを示します。 -
document.activeElement.nodeName
は選択している要素名を取得します。入力フォームのときINPUT
を返します。 - 4.1をα、4.2をβとすると ( α AND β )のときに動作させたくないので、ド・モルガンの法則で( !α OR !β )になります。
-
-
if (result.min <= leng && leng <= result.max) {}
: ポップアップから設定できる検索させる文字列の長さの条件です。 -
setTimeout(function() { if (str == window.getSelection().toString()) {} }, result.time * 1000)
-
setTimeout(function() {}, result.time * 1000)
は、ポップアップで設定したwaiting sec: N秒後に中身が実行されます。 -
if (str == window.getSelection().toString()) {}
は選択した文字が、N秒後も同じか判断します。( N秒間選択したままかどうか )
-
-
if (/youtube.com/.test(window.location.origin)) {} else {}
: YouTubeにいるか判断します。RegExpです。RegExp - JavaScript - MDN - Mozilla-
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