nicosearchで自動補完

  • /*
     * @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 MIT License
     */
    
    
    // 個人的にニコニコ静画、ニコニコ生放送などの"ニコニコ動画以外"を全く使わないので、動作検証は不十分です。
    // IE9以降と最新のFirefox, Chromeで動作すると思います。Operaは知りません。
    
    // 【直近の変更】
    // ・選択中の候補をカーソルキーで切り替える場合の上下端の処理を変更し、ループできるようにしました。
    
    
    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://www.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-box-sizing:  border-box;\
    			   -moz-box-sizing:  border-box;\
    			    -ms-box-sizing:  border-box;\
    			     -o-box-sizing:  border-box;\
    			        box-sizing:  border-box;\
    			-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;
    };
    
    
    // 枠外をクリックした時に suggestbox を削除
    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: // ↑
    				evt.preventDefault();
    				if (!suggestbox.querySelector(".selected")) {
    					walker.lastChild();
    					walker.currentNode.className = "selected";
    				}
    				else if (walker.previousNode()) {
    					walker.currentNode.nextElementSibling.className = "";
    					walker.currentNode.className = "selected";
    				}
    				else {
    					walker.currentNode.className = "";
    					while (walker.nextNode()){} // 最後まで移動
    					walker.currentNode.className = "selected";
    				}
    				valueArr.push(walker.currentNode.textContent);
    				searchBox.value = valueArr.join(" ");
    				break;
    			case 40: // ↓
    				evt.preventDefault();
    				if (!suggestbox.querySelector(".selected")) {
    					walker.firstChild();
    					walker.currentNode.className = "selected";
    				}
    				else if (walker.nextNode()) {
    					walker.currentNode.previousElementSibling.className = "";
    					walker.currentNode.className = "selected";
    				}
    				else {
    					walker.currentNode.className = "";
    					while (walker.previousNode()){} // 最初まで移動
    					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.slice.call(xml.getElementsByTagName("item"))
    	.map(function(val){
    		return val.textContent.split(QUERY_DELIMITER).join("_");
    	}).forEach(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);
    			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;
    			removeSuggestWindow();
    		}, false);
    		a.appendChild(img.cloneNode(false));
    		div.appendChild(a);
    		p.appendChild(document.createTextNode(val));
    		div.appendChild(p);
    		wrapper.appendChild(div);
    	});
    	document.body.appendChild(wrapper);
    }
    
    })();
  • Permalink
    このページへの個別リンクです。
    RAW
    書かれたコードへの直接のリンクです。
    Packed
    文字列が圧縮された書かれたコードへのリンクです。
    Userscript
    Greasemonkey 等で利用する場合の .user.js へのリンクです。
    Loader
    @require やソースコードが長い場合に多段ロードする Loader コミのコードへのリンクです。
    Metadata
    コード中にコメントで @xxx と書かれたメタデータの JSON です。

History

  1. 2013/05/28 03:43:42 - 2013-05-28
  2. 2013/05/25 20:26:56 - 2013-05-25
  3. 2013/05/25 20:21:07 - 2013-05-25
  4. 2013/05/25 20:14:54 - 2013-05-25
  5. 2013/05/25 01:46:27 - 2013-05-25
  6. 2013/05/21 19:07:56 - 2013-05-21
  7. 2013/05/18 02:03:22 - 2013-05-18
  8. 2013/05/15 21:49:02 - 2013-05-15
  9. 2013/05/15 21:21:32 - 2013-05-15
  10. 2013/05/15 00:33:54 - 2013-05-15
  11. 2013/05/15 00:16:22 - 2013-05-15
  12. 2013/05/15 00:15:10 - 2013-05-15
  13. 2013/05/13 18:36:18 - 2013-05-13
  14. 2013/05/13 15:54:32 - 2013-05-13
  15. 2013/05/13 15:23:01 - 2013-05-13
  16. 2013/05/13 15:08:07 - 2013-05-13
  17. 2013/05/13 08:57:59 - 2013-05-13
  18. 2013/05/13 08:44:37 - 2013-05-13
  19. 2013/05/13 08:20:17 - 2013-05-13
  20. 2013/05/13 08:17:31 - 2013-05-13