nicosearchで自動補完
by
htsign
2013-05-28 [2013/05/28 03:43:42]
niconicoにおいて、検索ワードのサジェストを表示します
@@ -18,8 +18,8 @@
// IE9以降と最新のFirefox, Chromeで動作すると思います。Operaは知りません。
// 【直近の変更】
-// ・ニコニコ大百科へのリンクがクリックできなかったのを修正しました。
-// ・長すぎる候補が出たとき、文字を丸めるようにしました。
+// ・AND検索に対応しました。
+// 適当な実装なので、全角スペースで区切っていても候補を選択すると半角スペース区切りになります。
if (!Element.prototype.matchesSelector) {
@@ -53,6 +53,7 @@
var REQUEST_METHOD = "post";
var REQUEST_URL = "http://nicoapi.com/ns/suggest.xml";
var RESPONSE_LENGTH = 10;
+var QUERY_DELIMITER = /[\s ]/;
var cache = {}; // 検索ワードと結果のペアを保持
var walker = null; // カーソルキーで動かした場合のTreeWalkerを保持
@@ -127,7 +128,6 @@
|| document.getElementById("search-box") // ニコニコ大百科
|| document.getElementById("jsSearchText") // SP版ニコニコ動画
|| document.querySelector("#globalSearch .searchString") // SP版ニコニコチャンネル
-
;
return target;
})();
@@ -172,6 +172,9 @@
}, false);
searchBox.addEventListener("keydown", function(evt){
+ var valueArr = searchBox.value.split(QUERY_DELIMITER);
+ valueArr.pop();
+
var suggestbox = document.getElementById("nicosearch-suggestbox");
if (suggestbox) {
if (!walker) walker = document.createTreeWalker(
@@ -194,7 +197,8 @@
walker.currentNode.nextElementSibling.className = "";
walker.currentNode.className = "selected";
}
- searchBox.value = walker.currentNode.textContent;
+ valueArr.push(walker.currentNode.textContent);
+ searchBox.value = valueArr.join(" ");
break;
case 40: // ↓
if (!suggestbox.querySelector(".selected")) {
@@ -205,7 +209,8 @@
walker.currentNode.previousElementSibling.className = "";
walker.currentNode.className = "selected";
}
- searchBox.value = walker.currentNode.textContent;
+ valueArr.push(walker.currentNode.textContent);
+ searchBox.value = valueArr.join(" ");
break;
case 13: // Enter
if (suggestbox.querySelector(".selected")) {
@@ -229,7 +234,8 @@
return false;
}
- var word = evt.target.value;
+ // 空白で区切った配列の最後を代入(AND検索用)
+ var word = evt.target.value.split(QUERY_DELIMITER).pop();
// 文字列が空の場合はサジェストを消す
if (word.length === 0) {
removeSuggestWindow();
/*
* @title nicosearchで自動補完
* @description niconicoにおいて、検索ワードのサジェストを表示します
* @include http://www.nicovideo.jp/*
* @include http://seiga.nicovideo.jp/*
* @include http://live.nicovideo.jp/*
* @include http://ch.nicovideo.jp/*
* @include http://com.nicovideo.jp/*
* @include http://dic.nicovideo.jp/*
* @include http://sp.nicovideo.jp/*
* @include http://sp.live.nicovideo.jp/*
* @include http://sp.ch.nicovideo.jp/*
* @license NYSL
*/
// 個人的にニコニコ静画、ニコニコ生放送などの"ニコニコ動画以外"を全く使わないので、動作検証は不十分です。
// IE9以降と最新のFirefox, Chromeで動作すると思います。Operaは知りません。
// 【直近の変更】
// ・AND検索に対応しました。
// 適当な実装なので、全角スペースで区切っていても候補を選択すると半角スペース区切りになります。
if (!Element.prototype.matchesSelector) {
Object.defineProperty(Element.prototype, "matchesSelector", {
enumerable: false,
configurable: true,
value: function(property){
if (this.webkitMatchesSelector) {
return this.webkitMatchesSelector(property);
}
else if (this.mozMatchesSelector) {
return this.mozMatchesSelector(property);
}
else if (this.msMatchesSelector) {
return this.msMatchesSelector(property);
}
else if (this.oMatchesSelector) {
return this.oMatchesSelector(property);
}
else
return null;
}
});
}
// ref: http://nicoapi.com/docs/nicosearchapi.html
// ref: https://chrome.google.com/webstore/detail/pafehjbkojbbenbdklnlajbmlojnecjl
(function main(){
var REQUEST_METHOD = "post";
var REQUEST_URL = "http://nicoapi.com/ns/suggest.xml";
var RESPONSE_LENGTH = 10;
var QUERY_DELIMITER = /[\s ]/;
var cache = {}; // 検索ワードと結果のペアを保持
var walker = null; // カーソルキーで動かした場合のTreeWalkerを保持
var tID = null; // sendRequestのtimeout用
(function createStyle(){
if (document.querySelector("#nicosearch-style")) {
return false;
}
var style = document.createElement("style"), i = 0;
style.id = "nicosearch-style";
document.getElementsByTagName("head")[0].appendChild(style);
([
"#nicosearch-suggestbox {\
position: absolute;\
border: 1px solid #778899;\
background-color: #fff;\
z-index: 100000;\
-webkit-user-select: none;\
-khtml-user-select: none;\
-moz-user-select: none;\
-ms-user-select: none;\
-o-user-select: none;\
user-select: none;\
}",
"#nicosearch-suggestbox * {\
text-align: left;\
}",
"#nicosearch-suggestbox > div {\
position: relative;\
line-height: normal;\
overflow: hidden;\
}",
"#nicosearch-suggestbox > div.selected\
,#nicosearch-suggestbox > div:hover {\
background-color: #afeeee;\
}",
"#nicosearch-suggestbox a {\
display: block;\
}",
".nicosearch-dic-icon {\
padding-top: 1px;\
padding-bottom: 1px;\
}",
".nicosearch-name {\
position: absolute;\
top: 0;\
left: 18px;\
width: calc(100% - 18px);\
height: 18px;\
font-size: 14px;\
text-overflow: ellipsis;\
word-wrap: normal;\
white-space: nowrap;\
overflow: hidden;\
cursor: pointer;\
}"
]).forEach(function(val){
style.sheet.insertRule(val, i++);
});
})();
var searchBox = (function getSearchBox(){
var target = document.getElementById("searchWord") // トップページ
|| document.getElementById("bar_search") // ニコニコ動画
|| document.getElementById("search_united") // 検索結果
|| document.querySelector(".searchText input") // 動画ページ
|| document.getElementById("bar_search") // ニコニコ静画
|| document.querySelector(".search_program .search_word") // ニコニコ生放送
|| document.getElementById("searchtxt") // ニコニコチャンネル
|| document.getElementById("search_text") // ニコニココミュニティ
|| document.getElementById("search-box") // ニコニコ大百科
|| document.getElementById("jsSearchText") // SP版ニコニコ動画
|| document.querySelector("#globalSearch .searchString") // SP版ニコニコチャンネル
;
return target;
})();
if (searchBox === null) {
window.alert("入力欄が見つかりません。");
return false;
}
var XmlHttp = function(word){
var xhr = new XMLHttpRequest();
xhr.open(REQUEST_METHOD, REQUEST_URL, true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
xhr.addEventListener("readystatechange", function(evt){
if (xhr.readyState === 4) {
switch(xhr.status) {
case 200:
cache[word.toLowerCase()] = xhr.responseXML; // キャッシュに追加
createSuggestWindow(xhr.responseXML);
break;
}
}
}, false);
return xhr;
};
// どうやらIE9はXMLHttpRequestをクロスドメインには使えないみたいで…
var Xdr = function(word, query){
var xdr = new XDomainRequest();
xdr.open("GET", REQUEST_URL + "?" + query);
xdr.onload = function(){
var xml = DOMParser().parseFromString(xdr.responseText, "application/xml");
cache[word.toLowerCase()] = xml; // キャッシュに追加
createSuggestWindow(xml);
};
return xdr;
};
document.addEventListener("click", function(evt){
if (!evt.target.matchesSelector("#nicosearch-suggestbox") || evt.target !== searchBox) {
removeSuggestWindow();
}
}, false);
searchBox.addEventListener("keydown", function(evt){
var valueArr = searchBox.value.split(QUERY_DELIMITER);
valueArr.pop();
var suggestbox = document.getElementById("nicosearch-suggestbox");
if (suggestbox) {
if (!walker) walker = document.createTreeWalker(
suggestbox,
NodeFilter.SHOW_ELEMENT,
function(node){
return node.parentElement === suggestbox
? NodeFilter.FILTER_ACCEPT
: NodeFilter.FILTER_SKIP;
},
false
);
switch(evt.keyCode) {
case 38: // ↑
if (!suggestbox.querySelector(".selected")) {
walker.lastChild();
walker.currentNode.className = "selected";
}
else if (walker.previousNode()) {
walker.currentNode.nextElementSibling.className = "";
walker.currentNode.className = "selected";
}
valueArr.push(walker.currentNode.textContent);
searchBox.value = valueArr.join(" ");
break;
case 40: // ↓
if (!suggestbox.querySelector(".selected")) {
walker.firstChild();
walker.currentNode.className = "selected";
}
else if (walker.nextNode()) {
walker.currentNode.previousElementSibling.className = "";
walker.currentNode.className = "selected";
}
valueArr.push(walker.currentNode.textContent);
searchBox.value = valueArr.join(" ");
break;
case 13: // Enter
if (suggestbox.querySelector(".selected")) {
walker.currentNode.getElementsByTagName("p")[0].click();
}
break;
default:
if (suggestbox.querySelector(".selected")) {
suggestbox.getElementsByClassName("selected")[0].className = "";
}
}
}
}, false);
searchBox.addEventListener("keyup", function(evt){
window.clearTimeout(tID);
// 選択状態の場合はなにもしない
var suggestbox = document.getElementById("nicosearch-suggestbox");
if (suggestbox && suggestbox.querySelector(".selected")) {
return false;
}
// 空白で区切った配列の最後を代入(AND検索用)
var word = evt.target.value.split(QUERY_DELIMITER).pop();
// 文字列が空の場合はサジェストを消す
if (word.length === 0) {
removeSuggestWindow();
return false;
}
// キャッシュをチェック
// 既に結果をもらっている場合は二度送らない
if (cache[word.toLowerCase()]) {
createSuggestWindow(cache[word.toLowerCase()]);
}
else {
tID = window.setTimeout(function(){
sendRequest(word);
}, 500);
}
}, false);
function sendRequest(word){
var xhr;
var query = "i=" + encodeURI(word) + "&l=" + RESPONSE_LENGTH;
try {
xhr = new XmlHttp(word);
} catch(e) {
xhr = new Xdr(word, query); // IE9用
}
xhr.send(query);
}
function removeSuggestWindow(){
var suggestbox = document.getElementById("nicosearch-suggestbox");
walker = null;
if (suggestbox) {
suggestbox.parentElement.removeChild(suggestbox);
}
}
function createSuggestWindow(xml){
if (xml.documentElement.getAttribute("status") !== "ok") return false;
walker = null;
var wrapper = document.getElementById("nicosearch-suggestbox");
if (!wrapper) {
wrapper = (function createWrapper(){
var wrapper = document.createElement("div");
var boxRect = searchBox.getBoundingClientRect();
wrapper.id = "nicosearch-suggestbox";
wrapper.style.top = boxRect.top + boxRect.height + "px";
wrapper.style.left = boxRect.left + "px";
wrapper.style.minWidth = searchBox.offsetWidth + "px";
return wrapper;
})();
}
else {
while (wrapper.hasChildNodes()) {
wrapper.removeChild(wrapper.firstChild);
}
}
var img = (function createImage(){
var img = document.createElement("img");
img.className = "nicosearch-dic-icon";
img.src = "data:image/png;base64,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";
return img;
})();
Array.prototype.forEach.call(xml.getElementsByTagName("item"), function(val){
var div = document.createElement("div");
if (document.ontouchstart) {
div.style.padding = "4px 0"; // SP版の場合ちょっと縦に幅を持たせる
}
var a = (function createAnchor(){
var a = document.createElement("a");
a.href = "http://dic.nicovideo.jp/a/" + encodeURIComponent(val.textContent);
a.target = "_blank";
return a;
})();
var p = (function createParagraph(){
var p = document.createElement("p");
p.className = "nicosearch-name";
return p;
})();
p.addEventListener("click", function(evt){
searchBox.value = val.textContent;
removeSuggestWindow();
}, false);
a.appendChild(img.cloneNode(false));
div.appendChild(a);
p.appendChild(document.createTextNode(val.textContent));
div.appendChild(p);
wrapper.appendChild(div);
});
document.body.appendChild(wrapper);
}
})();
- Permalink
- このページへの個別リンクです。
- RAW
- 書かれたコードへの直接のリンクです。
- Packed
- 文字列が圧縮された書かれたコードへのリンクです。
- Userscript
- Greasemonkey 等で利用する場合の .user.js へのリンクです。
- Loader
- @require やソースコードが長い場合に多段ロードする Loader コミのコードへのリンクです。
- Metadata
- コード中にコメントで @xxx と書かれたメタデータの JSON です。