1 /**
  2  * @fileOverview ポップアップを扱います。
  3  */
  4 
  5 /**
  6  * ポップアップの要素を作成し、その情報を保持します。
  7  * @class
  8  * @param {element} source        ポップアップとして表示する要素
  9  * @param {Boolean} [useFade]     フェードアウトを有効にするかどうか
 10  * @param {Boolean} [hideOnHover] ポップアップ上でホバーしたときに非表示にするかどうか
 11  */
 12 function PopupItem() {
 13     this.initialise.apply(this, arguments);
 14 }
 15 PopupItem.prototype = {
 16 	/**  ポップアップの親要素
 17 	 * @type element
 18 	 */
 19 	container:   null,
 20 	/**  ポップアップとして表示する要素の直近の親要素
 21 	 * @type element
 22  	 */
 23 	content:     null,
 24 	/**  ポップアップとして表示する要素
 25 	 * @type element
 26 	 */
 27 	source:      null,
 28 	/**  フェードアウトを有効にするかどうか
 29 	 * @type Boolean
 30 	 */
 31 	useFade:     false,
 32 	/**  ポップアップ上でホバーしたときに非表示にするかどうか
 33 	 * @type Boolean
 34 	 */
 35 	hideOnHover: false,
 36 	/** コンストラクタから自動的に呼ばれ、初期化処理を行います。 */
 37     initialise: function(source, useFade, hideOnHover) {
 38 		var divPopupContainer          = document.createElement("div");
 39 		divPopupContainer.className    = "popupContainer";
 40 		var divShadowTopRight          = document.createElement("div");
 41 		divShadowTopRight.className    = "shadowTopRight";
 42 		var divShadowBottomLeft        = document.createElement("div");
 43 		divShadowBottomLeft.className  = "shadowBottomLeft";
 44 		var divShadowBottomRight       = document.createElement("div");
 45 		divShadowBottomRight.className = "shadowBottomRight";
 46 		var divPopupBorder             = document.createElement("div");
 47 		divPopupBorder.className       = "popupBorder";
 48 		var divShadowRight             = document.createElement("div");
 49 		divShadowRight.className       = "shadowRight";
 50 		var divShadowBottom            = document.createElement("div");
 51 		divShadowBottom.className      = "shadowBottom";
 52 		var divPopupBody               = document.createElement("div");
 53 		divPopupBody.className         = "popupBody";
 54 		
 55 		divShadowBottom.appendChild(divPopupBody);
 56 		divShadowRight.appendChild(divShadowBottom);
 57 		divPopupBorder.appendChild(divShadowRight);
 58 		divShadowBottomRight.appendChild(divPopupBorder);
 59 		divShadowBottomLeft.appendChild(divShadowBottomRight);
 60 		divShadowTopRight.appendChild(divShadowBottomLeft);
 61 		divPopupContainer.appendChild(divShadowTopRight);
 62 		
 63 		this.source      = source;
 64 		this.container   = divPopupContainer;
 65 		this.content     = divPopupBody;
 66 		this.useFade     = useFade;
 67 		this.hideOnHover = hideOnHover;
 68     }
 69 };
 70 
 71 
 72 /**
 73  * 複数のポップアップをまとめて管理します。
 74  * @static
 75  */
 76 var Popup = {
 77 	/** PopupItem を格納した配列
 78 	 * @type Array
 79 	 */
 80 	items:       new Array(),
 81 	/** すでに初期化されたかどうかを表すフラグ
 82 	 * @type Boolean
 83 	 */
 84 	initialised: false,
 85 	/** 要素の位置とサイズを取得します。
 86 	 * @param  {element} src 要素
 87 	 * @return {object} left, top, width, height, right, bottom の各メンバから成るオブジェクト
 88 	 */
 89 	getRect: function(src) {
 90 		var parent = src;
 91 		var x = 0;
 92 		var y = 0;
 93 		while (parent) {
 94 			x += parent.offsetLeft;
 95 			y += parent.offsetTop;
 96 			parent = parent.offsetParent;
 97 		}
 98 		if (src.parentNode.tagName == "LABEL") y += window.scrollY - 
 99 			src.parentNode.parentNode.parentNode.scrollTop; // set y to the right position on the analyse dialog
100 		return {left: x, top: y, width: src.offsetWidth, height: src.offsetHeight,
101 		        right: x + src.offsetWidth, bottom: y + src.offsetHeight};
102 	},
103 	/** 指定された座標が要素の領域内にあるかどうかを調べます。
104 	 * @param  {Number}  x x 座標(スクリーン座標)
105 	 * @param  {Number}  y y 座標(スクリーン座標)
106 	 * @param  {element} element 要素
107 	 * @return {Boolean} 座標が要素の領域内にあれば true
108 	 */
109 	isPtInElement: function(x, y, element) {
110 		var rc = this.getRect(element);
111 		//return (x >= rc.left) && (x <= rc.right) && (y >= rc.top) && (y <= rc.bottom);
112 		return (x >= rc.left) && (x < rc.right) && (y >= rc.top) && (y < rc.bottom);
113 	},
114 	/** 要素がすでにポップアップとして存在しているかどうかを調べます。
115 	 * @param  {element} source 要素
116 	 * @return {Boolean} 要素がすでに存在していれば true
117 	 */
118 	sourceExists: function(source) {
119 		for (var i = 0; i < this.items.length; i++) {
120 			if (this.items[i].source == source) return true;
121 		}
122 		return false;
123 	},
124 	/** ポップアップのサイズに応じて、適切な位置に移動します。
125 	 * @param  {element} item 要素
126 	 */
127 	reposition: function(item) {
128 		var rectSource = this.getRect(item.source);
129 		item.container.style.left = rectSource.left;
130 		item.container.style.top  = rectSource.bottom;
131 		
132 		if (rectSource.bottom + item.container.offsetHeight > window.scrollY + window.innerHeight) {
133 			var oppositeY = (rectSource.top - item.container.offsetHeight);
134 			if (oppositeY >= window.scrollY + Nodes.header.offsetHeight) item.container.style.top = oppositeY;
135 		}
136 		
137 		if (item.container.offsetWidth > document.body.clientWidth) {
138 			item.container.style.left = 0;
139 		} else {
140 			if (rectSource.left + item.container.offsetWidth > window.scrollX + document.body.clientWidth)
141 				item.container.style.left = document.body.clientWidth - item.container.offsetWidth - window.scrollX;
142 		}
143 	},
144 	/** ポップアップとして要素を追加、表示します。
145 	 * @param  {element} content      表示する要素
146 	 * @param  {element} source       トリガー元の要素(位置の調整に使用される)
147 	 * @param  {Boolean} hideOnHover  ポップアップ上でホバーしたときに非表示にするかどうか
148 	 */
149 	add: function(content, source, hideOnHover) {
150 		if (this.sourceExists(source)) return;
151 		//var item = new PopupItem(source);
152 		var item = new PopupItem(source, SkinPref.getBool("enablePopupFade", true), hideOnHover);
153 		this.items.push(item);
154 		item.content.appendChild(content);
155 		document.body.appendChild(item.container);
156 		this.reposition(item);
157 		
158 		if (!this.initialised) {
159 			window.addEventListener("mouseout",  this.remove.bind(this), false);
160 			window.addEventListener("mousemove", this.remove.bind(this), false);
161 			this.initialised = true;
162 		}
163 
164 		return item;
165 	},
166 	/** ポップアップを削除します。
167 	 * @param  {event} e イベント
168 	 */
169 	remove: function(e) {
170 		if (ResNodes.isContextMenuVisible()) return;
171 		var x = e.pageX;
172 		var y = e.pageY;
173 		for (var i = this.items.length - 1; i >= 0; i--) {
174 			if (this.isPtInElement(x, y, this.items[i].source))    return;
175 			if (!this.items[i].hideOnHover) if (this.isPtInElement(x, y, this.items[i].container)) return;
176 			//if (SkinPref.getBool("enablePopupFade", true)) {
177 			if (this.items[i].useFade) {
178 				this.removeChildWithFade(this.items[i].container);
179 			} else {
180 				document.body.removeChild(this.items[i].container);
181 			}
182 			this.items.pop();
183 		}
184 	},
185 	/** 指定された要素が載っているポップアップを探します。
186 	 * @param  {element} content 要素
187 	 * @return {PopupItem} マッチした PopupItem
188 	 */
189 	findPopup: function(content) {
190 		for (var i = 0; i < this.items.length; i++) {
191 			if (this.items[i].content.firstChild == content) return this.items[i];
192 		}
193 		return null;
194 	},
195 	/** ポップアップのフェードアウト処理を行います。removeChildWithFade() から呼ばれます。
196 	 * @param  {String} id 要素の ID
197 	 */
198 	fadeOutProc: function(id) {
199 		var node = document.getElementById(id);
200 		if (node) {
201 			var fadeStep = SkinPref.getInt("valuePopupFadeStep", 1);
202 			switch(fadeStep){
203 			case 0:
204 				fadeStep = 0.1;  break;
205 			case 1:
206 				fadeStep = 0.2;  break;
207 			case 2:
208 				fadeStep = 0.25; break;
209 			case 3:
210 				fadeStep = 0.33; break;
211 			case 4:
212 				fadeStep = 0.5;  break;
213 			default:
214 				fadeStep = 0.2;
215 			}
216 			node.style.opacity = parseFloat(node.style.opacity) - fadeStep;
217 			if (parseFloat(node.style.opacity) <= 0.0) {
218 				document.body.removeChild(node);
219 			} else {
220 				window.setTimeout(this.fadeOutProc.bind(this), 1, id);
221 			}
222 		}
223 	},
224 	/** ポップアップをフェードアウトしながら削除します。
225 	 * @param  {element} node 要素
226 	 */
227 	removeChildWithFade: function(node){
228 		var id = "#" + new Date().getTime() + Math.random();
229 		node.id = id
230 		node.style.opacity = 1.0;
231 		window.setTimeout(this.fadeOutProc.bind(this), 1, id);
232 	}
233 };
234 
235 // ResPopup オブジェクト
236 // レス番ポップアップ(と名前欄ポップアップ)
237 //
238 /**
239  * レスアンカーと名前欄のポップアップを管理します。
240  * @static
241  */
242 var ResPopup = {
243 	/** イベントリスナを登録します。*/
244 	initialise: function() {
245 		window.addEventListener("mouseover", this.onMouseOver.bind(this), false);
246 	},
247 	/** 全角文字で表記された数字を、数値型に変換します。
248 	 * @param  {String} str 全角数字が含まれた文字列
249 	 * @return {Number} 変換された数値
250 	 */
251 	toNarrow: function(str) {
252 		return parseInt(str.replace(/([0-9])/g, function(n){ return String.fromCharCode(n.charCodeAt(0) - 0xFEE0); }));
253 	},
254 	/** 要素を複製し、いくつかの属性を削除したものを返却します。
255 	 * @param  {element} src 要素
256 	 * @return {element} 複製された要素
257 	 */
258 	getCloneNode: function(src){
259 		var dest = src.cloneNode(true);
260 		if (dest.id) dest.id = "";
261 		dest.removeAttribute("name");
262 		dest.style.display = "block";
263 		var e = dest.getElementsByTagName("*");
264 		for(var i=0; i<e.length; i++){
265 			if(e[i].id) e[i].id = "";
266 			e[i].removeAttribute("name");
267 		}
268 		return dest;
269 	},
270 	/** mouseover イベントを処理します。
271 	 * @param  {event} e イベント
272 	 */
273 	onMouseOver: function(e) {
274 		var node = e.target;
275 		switch (node.className) {
276 			case "resPointer":
277 				if (node.textContent.match(/(>?>|>)(\d{1,4})-(\d{1,4})/)) {
278 					this.show(node, this.toNarrow(RegExp.$2), this.toNarrow(RegExp.$3));
279 				} else if (node.textContent.match(/(>?>|>)(\d{1,4})/)) {
280 					this.show(node, this.toNarrow(RegExp.$2), this.toNarrow(RegExp.$2));
281 				}
282 				break;
283 			case "resName":
284 				if (node.textContent.match(/^(\d{1,4})-(\d{1,4})$/)) {
285 					node.style.cursor = "pointer";
286 					this.show(node, this.toNarrow(RegExp.$1), this.toNarrow(RegExp.$2));
287 					node.setAttribute("onclick", "ThreadDocument.jumpTo(event)")
288 				} else if (node.textContent.match(/^(\d{1,4})$/)) {
289 					node.style.cursor = "pointer";
290 					this.show(node, this.toNarrow(RegExp.$1), this.toNarrow(RegExp.$1));
291 					node.setAttribute("onclick", "ThreadDocument.jumpTo(event)")
292 				}
293 		}
294 	},
295 	/** ポップアップを表示します。onMouseOver() から呼ばれます。
296 	 * @param {element} source トリガー元の要素
297 	 * @param {Number}  start  ポップアップを開始するレス番号
298 	 * @param {Number}  end    ポップアップを終了するレス番号
299 	 */
300 	show: function(source, start, end) {
301 		const POPUP_LIMIT = 20;
302 		if (!SkinPref.getBool("enableResPopup", true)) return;
303 		if (end < start) return;
304 		if (start < 1)  start = 1;
305 		if (end > 1000) end = 1000;
306 		if ((end - start) > POPUP_LIMIT) end = start + POPUP_LIMIT;
307 		Trackback.traverse();
308 		var content = document.createDocumentFragment();
309 		for(var i = start; i <= end; i++){
310 			resNode = Nodes.getRes(i);
311 			if(!resNode) {
312 				this.showExceeded(source, start, end);
313 				return;
314 			}
315 			var dlPopup = this.getCloneNode(resNode);
316 			dlPopup.className = "resPopup";
317 			Trackback.appendTo(dlPopup, start);
318 			content.appendChild(dlPopup);
319 		}
320 		var popupItem = Popup.add(content, source);
321 		// error ...
322 		if (popupItem) ThreadDocument.modifyAnchors(popupItem.content.parentNode);
323 	},
324 	/** 表示域外のポップアップを表示します。onMouseOver() から呼ばれます。
325 	 * @param {element} source トリガー元の要素
326 	 * @param {Number}  start  ポップアップを開始するレス番号
327 	 * @param {Number}  end    ポップアップを終了するレス番号
328 	 */
329 	showExceeded: function(source, start, end) {
330 	    var xmlHTTP = new XMLHttpRequest();
331 	    xmlHTTP.onreadystatechange = xmlEventHandler.bind(this);
332 	    function xmlEventHandler() {
333 	        if (xmlHTTP.readyState == 4) {
334 	            if (xmlHTTP.status == 200) {
335 					var content = document.createDocumentFragment();
336 					var divTemp  = document.createElement("div");
337 	                divTemp.innerHTML = xmlHTTP.responseText;
338 	                var dl = divTemp.getElementsByTagName("dl");
339 					for(var i = 0; i < dl.length; i++){
340 						var dlPopup = this.getCloneNode(dl[i]);
341 						dlPopup.className = "resPopup";
342 						Trackback.appendTo(dlPopup, start);
343 						content.appendChild(dlPopup);
344 					}
345 					var popupItem = Popup.add(content, source);
346 					if (popupItem) ThreadDocument.modifyAnchors(popupItem.content);
347 	            }
348 	        }
349 	    }
350     	xmlHTTP.open("GET", SERVER_URL + EXACT_URL + start + "-" + end + "n" + "?lite=true", true);
351 	    xmlHTTP.send(null);
352 	}
353 };
354 
355 /**
356  * ID のポップアップを管理します。
357  * @static
358  */
359 var IDPopup = {
360 	/** イベントリスナを登録します。*/
361 	initialise: function() {
362 		if (SkinPref.getBool("enableIDPopupOnClick", false)) {
363 			window.addEventListener("click", this.onMouseOver.bind(this), false);
364 		} else {
365 			window.addEventListener("mouseover", this.onMouseOver.bind(this), false);
366 		}
367 	},
368 	/** 要素を複製し、いくつかの属性を削除したものを返却します。
369 	 * @param  {element} src 要素
370 	 * @return {element} 複製された要素
371 	 */
372 	getCloneNode: function(src){
373 		var dest = src.cloneNode(true);
374 		if (dest.id) dest.id = "";
375 		dest.removeAttribute("name");
376 		dest.style.display = "block";
377 		var e = dest.getElementsByTagName("*");
378 		for(var i=0; i<e.length; i++){
379 			if(e[i].id) e[i].id = "";
380 			e[i].removeAttribute("name");
381 		}
382 		return dest;
383 	},
384 	/** mouseover イベントを処理します。
385 	 * @param  {event} e イベント
386 	 */
387 	onMouseOver: function(e) {
388 		var node = e.target;
389 		if (node.tagName == "SPAN") { // avoid conflict between Moreyon
390 			if (node.className.match(/mesID_/)) {
391 				var pos = node.className.indexOf(" ");
392 				var className = (pos == -1) ? node.className : node.className.slice(0, pos);
393 				IDPopup.show(node, className.slice(6), true);
394 			} else if (node.className.match(/^resID/)) {
395 				IDPopup.show(node, node.getAttribute("rel"), false);
396 			}
397 		}
398 	},
399 	/** ポップアップを表示します。onMouseOver() から呼ばれます。
400 	 * @param {element} source      トリガー元の要素
401 	 * @param {Number}  id          ポップアップする ID
402 	 * @param {bool}    popupOnBody ポップアップを終了するレス番号
403 	 */
404 	show: function(source, id, popupOnBody) {
405 		if (!SkinPref.getBool("enableIDPopup", true)) return;
406 		if (id.length < 8) return;
407 		const IDENTIFIER = "IDPopup:";
408 		if (document.getElementById(IDENTIFIER + id)) return;
409 		ID.traverse();
410 		var items = ID.items[id];
411 		if (!items) return;
412 		if (items.length < (popupOnBody ? 1 : 2)) return;
413 		
414 		var appendAll = SkinPref.getBool("enableIDPopupAll", false);
415 		var content = document.createDocumentFragment();		
416 		var div = document.createElement("div");
417 		div.id = IDENTIFIER + id;
418 		div.className = "popupHeader";
419 		div.appendChild(appendAll ? source.cloneNode(true) : document.createTextNode("参照"));
420 		div.appendChild(document.createTextNode(": "));
421 		content.appendChild(div);
422 		var a = document.createElement("a");
423 		a.className = "resPointer";
424 		a.href = "javascript:void(0)";
425 		a.onclick = ThreadDocument.jumpTo.bind(ThreadDocument);
426 
427 		for (var i = 0; i < items.length; i++) {
428 			if (appendAll) {
429 				var dlPopup = this.getCloneNode(Nodes.getRes(items[i]));
430 				dlPopup.className = "resPopup";
431 				Trackback.appendTo(dlPopup, items[i]);
432 				content.appendChild(dlPopup);
433 			}
434 			var anchor = a.cloneNode(false);
435 			anchor.appendChild(document.createTextNode(">>" + items[i]));
436 			div.appendChild(anchor);
437 		}
438 		
439 		var nodePopup = Popup.add(content, source);
440 		if (!appendAll) nodePopup.container.className = "popupResList";
441 	}
442 };
443 
444 /**
445  * 逆参照のポップアップを管理します。
446  * @static
447  */
448 var TrackbackPopup = {
449 	/** イベントリスナを登録します。*/
450 	initialise: function() {
451 		window.addEventListener("mouseover", this.onMouseOver.bind(this), false);
452 	},
453 	/** 要素を複製し、いくつかの属性を削除したものを返却します。
454 	 * @param  {element} src 要素
455 	 * @return {element} 複製された要素
456 	 */
457 	getCloneNode: function(src){
458 		var dest = src.cloneNode(true);
459 		if (dest.id) dest.id = "";
460 		dest.removeAttribute("name");
461 		dest.style.display = "block";
462 		var e = dest.getElementsByTagName("*");
463 		for(var i=0; i<e.length; i++){
464 			if(e[i].id) e[i].id = "";
465 			e[i].removeAttribute("name");
466 		}
467 		return dest;
468 	},
469 	/** mouseover イベントを処理します。
470 	 * @param  {event} e イベント
471 	 */
472 	onMouseOver: function(e) {
473 		var node = e.target;
474 		if (node.tagName == "A") if (node.className == "trackback") {
475 			if (node.textContent.match(/^(\d{1,4})$/)) {
476 				this.show(node, RegExp.$1);
477 			}
478 		}
479 	},
480 	/** ポップアップを表示します。onMouseOver() から呼ばれます。
481 	 * @param {element} source      トリガー元の要素
482 	 * @param {Number}  number      ポップアップするレス番号
483 	 */
484 	show: function(source, number) {
485 		if (!SkinPref.getBool("enableTrackBackPopup", true)) return;
486 		Trackback.traverse();
487 		var tb = Trackback.items[number];
488 		if (tb.length < 1) return;
489 		var appendAll = SkinPref.getBool("enableTrackBackPopupAll", false) || tb.length == 1;
490 		var content = document.createDocumentFragment();
491 		if (appendAll) {
492 			for (var i = 0; i < tb.length; i++) {
493 				var node = this.getCloneNode(ResNodes.getContainerByIndex(tb[i]));
494 				node.className = "resPopup";
495 				Trackback.appendTo(node, tb[i]);
496 				content.appendChild(node);
497 			}
498 		} else {
499 			var div = document.createElement("div");
500 			div.className = "popupHeader";
501 			div.appendChild(document.createTextNode("参照: "));
502 			div.appendChild(Trackback.getAnchorElements(number));
503 			content.appendChild(div);
504 		}
505 		var nodePopup = Popup.add(content, source);
506 		if (!appendAll) nodePopup.container.className = "popupResList";
507 		if (nodePopup) ThreadDocument.modifyAnchors(nodePopup.content.parentNode);
508 	}
509 };
510 
511 /**
512  * 画像のポップアップを管理します。
513  * @static
514  */
515 var ImagePopup = {
516 	/** 画像の URI を判別する正規表現
517 	 * @type RegExp */
518 	regExp: /\.(jpg|jpeg|png|gif|mng|tiff|tif|bmp|pict)$/i,
519 	/** 危険なレスかどうかを判別する正規表現
520 	 * @type RegExp */
521 	grotesqueRegExp: /(グロ|グロ|注意|危険|有害|ブラクラ|ブラクラ|精神|蓮|氏ね|死|血)/,
522 	/** イベントリスナを登録します。*/
523 	initialise: function() {
524 		window.addEventListener("mouseover", this.onMouseOver.bind(this), false);
525 	},
526 	/** 対象の URI が画像かどうかを判別します。
527 	 * @param  {String}  src URI
528 	 * @return {Boolean} 画像であれば true
529 	 */
530 	isImage: function(src) {
531 		return this.regExp.test(src);
532 	},
533 	/** 対象のレス番号にグロテスク画像への注意を喚起するレスがついているか調べます。
534 	 * @param  {Number} index レス番号
535 	 * @return {Boolean} 危険なレスであれば true
536 	 */
537 	isImageGrotesque: function(index) {
538 		Trackback.traverse();
539 		var nodes = Trackback.items[index];
540 		if (nodes) {
541 			for (var i = 0; i < nodes.length; i++) {
542 				var node = ResNodes.getBodyByIndex(nodes[i]);
543 				if (node) {
544 					if (node.textContent.match(this.grotesqueRegExp)) {
545 						return true;
546 					}
547 				}
548 			}
549 		}
550 		return false;
551 	},
552 	/** mouseover イベントを処理します。
553 	 * @param  {event} e イベント
554 	 */
555 	onMouseOver: function(e) {
556 		var node = e.target;
557 		if (node.className != "outLink") return;
558 		//var src = node.href;
559 		var src = node.rel;
560 		//if (src.match(/\?url=/)) src = RegExp.rightContext;
561 		if (this.isImage(src)) {
562 			if (e.ctrlKey ^ SkinPref.getBool("enableDefaultPixelation", false)) {
563 				if (SkinPref.getInt("valuePixelationMethod", 0) == 0) {
564 					this.pixelateShow(node, src);
565 				} else {
566 					this.blurShow(node, src);
567 				}
568 			} else {
569 				if (this.isImageGrotesque(ResNodes.getIndexByBody(node.parentNode))) {
570 					if (SkinPref.getInt("valuePixelationMethod", 0) == 0) {
571 						this.pixelateShow(node, src);
572 					} else {
573 						this.blurShow(node, src);
574 					}
575 				} else {
576 					this.show(node, src);
577 				}
578 			}
579 		}
580 	},
581 	/** click イベントを処理します。
582 	 * @param  {event} e イベント
583 	 */
584 	onClick: function(e) {
585 		var node  = e.currentTarget;
586 		var popup = Popup.findPopup(node);
587 		if (popup) {
588 			if (node.className == "imgPopup") {
589 				node.className =  "imgLargePopup";
590 				popup.content.style.maxWidth  = "none";
591 				popup.content.style.maxHeight = "none";
592 				popup.content.style.maxWidth  = e.currentTarget.offsetWidth  - 7;
593 				popup.content.style.maxHeight = e.currentTarget.offsetHeight - 7;
594 			} else {
595 				if (node.tagName.match(/(canvas|svg)/i)) {
596 					node.parentNode.childNodes[1].style.opacity = 1;
597 					node.parentNode.removeChild(node);
598 				} else {
599 					e.currentTarget.className =  "imgPopup";
600 					popup.content.style.maxWidth  = e.currentTarget.offsetWidth  - 7;
601 					popup.content.style.maxHeight = e.currentTarget.offsetHeight - 7;
602 				}
603 			}
604 		}
605 	},
606 	/** error イベントを処理します。
607 	 * @param  {event} e イベント
608 	 */
609 	onError: function(e) {
610 		//var popup = Popup.findPopup(e.currentTarget.parentNode);
611 		var popup = Popup.findPopup(e.currentTarget);
612 		if (popup) {
613 			popup.source.setAttribute("title", "エラー");
614 		}
615 		e.currentTarget.parentNode.removeChild(e.currentTarget);
616 	},
617 	/** 画像のリサイズを監視します。show() から呼ばれます。画像のサイズが確定すると、ポップアップのサイズを調整し実際に表示します。
618 	 * @param  {PopupItem} nodePopup PopupItem オブジェクト
619 	 */
620 	resize: function(nodePopup) {
621 		if (!nodePopup) return;
622 		if (!nodePopup.content.lastChild) return;
623 		if ((nodePopup.content.lastChild.offsetWidth != 0) && (nodePopup.content.lastChild.offsetHeight != 0)) {
624 			if ((nodePopup.content.lastChild.offsetWidth != 24) && (nodePopup.content.lastChild.offsetHeight != 24)) {
625 			//if ((nodePopup.content.firstChild.offsetWidth != 28) && (nodePopup.content.firstChild.offsetHeight != 28)) {
626 				//nodePopup.container.className = "popupImage";
627 				//nodePopup.content.firstChild.className = "imgPopup";
628 				nodePopup.content.style.maxWidth  = nodePopup.content.lastChild.offsetWidth  - 7;
629 				nodePopup.content.style.maxHeight = nodePopup.content.lastChild.offsetHeight - 7;
630 				Popup.reposition(nodePopup);
631 				nodePopup.container.style.visibility = "visible";
632 				return;
633 			}
634 		}
635 		window.setTimeout(this.resize.bind(this), 50, nodePopup);
636 	},
637 	/** ポップアップを表示します。onMouseOver() から呼ばれます。
638 	 * @param {element} source   トリガー元の要素
639 	 * @param {String}  href      画像の URI
640 	 */
641 	show: function(source, href) {
642 		if (!SkinPref.getBool("enableImagePopup", true)) return;
643 		source.removeAttribute("title");
644 		var img = document.createElement("img");
645 		img.onerror = this.onError.bind(this);
646 		img.onclick = this.onClick.bind(this);
647 		var nodePopup = Popup.add(img, source);
648 		if (nodePopup) {
649 			nodePopup.container.className = "popupImage";
650 			img.className = "imgPopup";
651 			nodePopup.container.style.visibility = "hidden";
652 			nodePopup.useFade = false;
653 			//alert("");
654 			img.src = href;
655 			window.setTimeout(this.resize.bind(this), 50, nodePopup);
656 		}
657 	},
658 	/** 画像をモザイク化してポップアップを表示します。onMouseOver() から呼ばれます。
659 	 * @param {element} source   トリガー元の要素
660 	 * @param {String}  href      画像の URI
661 	 */
662 	pixelateShow: function(source, href) {
663 		if (!SkinPref.getBool("enableImagePopup", true)) return;
664 		source.removeAttribute("title");
665 		var img = document.createElement("img");
666 		img.onerror = this.onError.bind(this);
667 		img.onclick = this.onClick.bind(this);
668 		img.style.opacity = 0.2;
669 		var nodePopup = Popup.add(img, source);
670 		var f = (function() {
671 			var canvas = document.createElement("canvas");
672 			canvas.onclick   = this.onClick.bind(this);
673 			canvas.style.position   = "absolute";
674 			canvas.style.zIndex = 1;
675 			canvas.width  = img.offsetWidth;
676 			canvas.height = img.offsetHeight;
677 			switch (SkinPref.getInt("valuePixelationSize", 1)) {
678 				case 0:
679 					var px = 8; break;
680 				case 1:
681 					var px = 4; break;
682 				case 2:
683 					var px = 2; break;
684 			}
685 			var ctx = canvas.getContext('2d');
686 			ctx.globalCompositeOperation = "copy";
687 			var w = canvas.width  / px;
688 			var h = canvas.height / px;
689 			ctx.drawImage(img, 0, 0, w, h);
690 			ctx.drawImage(canvas, 0, 0, w, h, 0, 0, canvas.width + px, canvas.height + px);
691 			if (SkinPref.getBool("enablePrecisePixelation", false)) {
692 				var m = px / 2;
693 				for (var y = 0; y < canvas.height; y += px) {
694 					for (var x = 0; x < canvas.width; x += px) {
695 						ctx.drawImage(canvas, x + m, y + m, 1, 1, x, y, px, px);
696 					}
697 				}
698 			}
699 			nodePopup.content.insertBefore(canvas, nodePopup.content.firstChild);
700 		}).bind(this);
701 		if (nodePopup) {
702 			nodePopup.container.className = "popupImage";
703 			img.className = "imgPopup";
704 			nodePopup.container.style.visibility = "hidden";
705 			nodePopup.useFade = false;
706 			img.onload = f;
707 			window.setTimeout(this.resize.bind(this), 50, nodePopup);
708 			img.src = href;
709 		}
710 	},
711 	/** onMouseOver() 
712 	 * @param {element} source   
713 	 * @param {String}  href       URI
714 	 */
715 	blurShow: function(source, href) {
716 		if (!SkinPref.getBool("enableImagePopup", true)) return;
717 		source.removeAttribute("title");
718 		var img = document.createElement("img");
719 		img.style.opacity = 0.2;
720 		img.onerror = this.onError.bind(this);
721 		img.onclick = this.onClick.bind(this);
722 		var nodePopup = Popup.add(img, source);
723 		var f = (function() {
724 			switch (SkinPref.getInt("valuePixelationSize", 1)) {
725 				case 0:
726 					var px = 8; break;
727 				case 1:
728 					var px = 4; break;
729 				case 2:
730 					var px = 2; break;
731 			}
732 			var canvas = new SVGCanvas(img.offsetWidth, img.offsetHeight);
733 			canvas.svg.onclick = this.onClick.bind(this);
734 			canvas.svg.style.position  = "absolute";
735 			canvas.svg.style.zIndex = 1;
736 			var defs   = new SVGDefinitions();
737 			var filter = new SVGFilter("blur");
738 			var effect = new SVGFilterPrimitives.GaussianBlur(px);
739 			filter.add(effect);
740 			defs.add(filter);
741 			var layer = new SVGLayer();
742 			var element = layer.image(img.src, 0, 0, img.offsetWidth, img.offsetHeight);
743 			element.setAttribute("filter", "url(#blur)");
744 			canvas.add(defs);
745 			canvas.add(layer);
746 			nodePopup.content.insertBefore(canvas.svg, nodePopup.content.firstChild);
747 		}).bind(this);
748 		if (nodePopup) {
749 			nodePopup.container.className = "popupImage";
750 			img.className = "imgPopup";
751 			nodePopup.container.style.visibility = "hidden";
752 			nodePopup.useFade = false;
753 			img.onload = f;
754 			window.setTimeout(this.resize.bind(this), 50, nodePopup);
755 			img.src = href;
756 		}
757 	}
758 };
759 
760 /**
761  * 
762  * @static
763  */
764 var UrlPopup = {
765 	/** */
766 	initialise: function() {
767 		window.addEventListener("mouseover", this.onMouseOver.bind(this), false);
768 	},
769 	/** URI 
770 	 * @param  {String} src URI
771 	 * @return {Boolean}  true
772 	 */
773 	isValid: function(src) {
774 		if (src.match(/(http|https):\/\/.+\/.*\.([a-zA-Z0-9]+)$/)) {
775 			var ext = RegExp.$2;
776 			return ext.match(/^(htm|html|shtm|shtml|stm|xml|xhtml|php|php3|php4|cgi|jsp|cfm|asp|aspx|pl|plx|py|rb|)$/);
777 		} else {
778 			return true;
779 		}
780 	},
781 	/** mouseover イベントを処理します。
782 	 * @param  {event} e イベント
783 	 */
784 	onMouseOver: function(e) {
785 		var node = e.target;
786 		if (node.className != "outLink") return;
787 		//var src = node.href;
788 		var src = node.rel;
789 		//if (src.match(/\?url=/)) src = RegExp.rightContext;
790 		if (src.match(/2ch.net/)) return;
791 		if (src.match(/read\.cgi/)) return;
792 		if (ImagePopup.isImage(src)) return;
793 		if (VideoPopup.getVideoSource(src)) return;
794 		if (this.isValid(src)) this.show(node, src);
795 	},
796 	/** error イベントを処理します。
797 	 * @param  {event} e イベント
798 	 */
799 	onError: function(e) {
800 		var popup = Popup.findPopup(e.currentTarget.parentNode);
801 		if (popup) {
802 			popup.source.setAttribute("title", "エラー");
803 		}
804 		e.currentTarget.parentNode.removeChild(e.currentTarget);
805 	},
806 	/** 画像のリサイズを監視します。show() から呼ばれます。画像のサイズが確定すると、ポップアップのサイズを調整し実際に表示します。
807 	 * @param  {PopupItem} nodePopup PopupItem オブジェクト
808 	 */
809 	resize: function(nodePopup) {
810 		if (!nodePopup) return;
811 		if (!nodePopup.content.firstChild.firstChild) return;
812 		if ((nodePopup.content.firstChild.firstChild.offsetWidth != 0) && (nodePopup.content.firstChild.firstChild.offsetHeight != 0)) {
813 			if ((nodePopup.content.firstChild.firstChild.offsetWidth != 28) && (nodePopup.content.firstChild.firstChild.offsetHeight != 28)) {
814 				nodePopup.container.className = "popupImage";
815 				nodePopup.content.firstChild.firstChild.className = "urlPopup";
816 				nodePopup.content.style.maxWidth  = nodePopup.content.firstChild.firstChild.offsetWidth  - 7;
817 				nodePopup.content.style.maxHeight = nodePopup.content.firstChild.firstChild.offsetHeight - 7;
818 				Popup.reposition(nodePopup);
819 				nodePopup.container.style.visibility = "visible";
820 				return;
821 			}
822 		}
823 		window.setTimeout(this.resize.bind(this), 10, nodePopup);
824 	},
825 	/** ポップアップを表示します。onMouseOver() から呼ばれます。
826 	 * @param {element} source   トリガー元の要素
827 	 * @param {String}  href     リンク先の URI
828 	 */
829 	show: function(source, href) {
830 		if (!SkinPref.getBool("enableUrlPopup", true)) return;
831 		source.removeAttribute("title");
832 		var a    = document.createElement("a");
833 		a.href   = href;
834 		if (SkinPref.getBool("enableNewWindow", true)) a.target = "_blank";
835 		var img = document.createElement("img");
836 		img.src = "http://shots.snap.com/preview/?url=" + encodeURIComponent(href) + "&key=9101c9d3294ad9f8c6627143df504a3a&src=www.snap.com&cp=&sb=1&v=2.19.1&size=" +
837 			(SkinPref.getInt("valueUrlPopupSize", 0) == 0 ? "large" : "small");
838 		img.setAttribute("onerror", "UrlPopup.onError(event)");
839 		a.appendChild(img);
840 		var nodePopup = Popup.add(a, source, true);
841 		if (nodePopup) {
842 			nodePopup.container.style.visibility = "hidden";
843 			window.setTimeout(this.resize.bind(this), 10, nodePopup);
844 		}
845 	}
846 };
847 
848 
849 /**
850  * 動画サイトのポップアップを管理します。
851  * @static
852  */
853 var VideoPopup = {
854 	/** イベントリスナを登録します。*/
855 	initialise: function() {
856 		window.addEventListener("mouseover", this.onMouseOver.bind(this), false);
857 	},
858 	/** URI から動画のプレビューのための正しいソース URI を取得します。
859 	 * @param  {String} src URI
860 	 * @return {String} ソース URI
861 	 */
862 	getVideoSource: function(src) {
863 		//if (src.match(/youtube\.com\/.*\?v=([^\&]+)/)) {
864 		if (src.match(/youtube\.com\/watch\?v=([^&]+)/)) {
865 			var option = SkinPref.getBool("videoPopupAutoStart", true) ? "&autoplay=1" : "";
866       		return "http://www.youtube.com/v/" + RegExp.$1 + option;
867 	    } else if (src.match(/stage6.divx.com\/.*\?content_id=(\d+)/)) {
868 	    	return "http://images.stage6.com/videos/" + RegExp.$1 + ".jpg";
869 	    } else if (src.match(/stage6.divx.com\/.*\/show_video\/(\d+)/)) {
870 	    	return "http://images.stage6.com/videos/" + RegExp.$1 + ".jpg";
871 	    } else if (src.match(/stage6.divx.com\/members\/(\d+)\/videos\/(\d+)/)) {
872 	    	return "http://video.stage6.com/" + RegExp.$1 + "/" + RegExp.$2 + ".divx";
873 	    } else if (src.match(/video.google.*\?docid=([\d\-]+)/)) {
874 	    	return "http://video.google.com/googleplayer.swf?docId=" + RegExp.$1;
875 	    } else if (src.match(/metacafe.com\/watch\/(\d+)/)){
876 	    	return "http://www.metacafe.com/fplayer/" + RegExp.$1 + ".swf";
877 	    } else if (src.match(/guba.com\/watch\/(\d+)/)){
878 	    	return "http://www.guba.com/f/root.swf?video_url=http://free.guba.com/uploaditem/" + RegExp.$1 + "/flash.flv&isEmbeddedPlayer=true";
879 	    } else if (src.match(/nicovideo.jp\/watch\/([^\&]+)/)){
880 	    	//return "http://nicopon.jp/video/FlowPlayer.swf?config={videoFile:%20'http://nicopon.jp/video/src/" + RegExp.$1 + "',%20initialScale:'fit'}";
881 	    	return "http://www.nicovideo.jp/thumb/" + RegExp.$1
882 	    } else if (src.match(/nicovideo.jp\/.*\?v=([^\&]+)/)){
883 	    	//return "http://nicopon.jp/video/FlowPlayer.swf?config={videoFile:%20'http://nicopon.jp/video/src/" + RegExp.$1 + "',%20initialScale:'fit'}";
884 	    	return "http://www.nicovideo.jp/thumb/" + RegExp.$1
885 	    } else {
886 	    	return false;
887 	    }
888 	},
889 	/** mouseover イベントを処理します。
890 	 * @param  {event} e イベント
891 	 */
892 	onMouseOver: function(e) {
893 		var node = e.target;
894 		if (node.className != "outLink") return;
895 		//var src = node.href;
896 		var src = node.rel;
897 		//if (src.match(/\?url=/)) src = RegExp.rightContext;
898 		if (ImagePopup.isImage(src)) return;
899 		if (src.match(/2ch.net/)) return;
900 		var href = this.getVideoSource(src);
901 		if (href) this.show(node, href);
902 	},
903 	/** ポップアップを表示します。onMouseOver() から呼ばれます。
904 	 * @param {element} source   トリガー元の要素
905 	 * @param {String}  href     ソース URI
906 	 */
907 	show: function(source, href) {
908 		if (!SkinPref.getBool("enableVideoPopup", true)) return;
909 		if (href.match(/\.jpg$/)) {
910 			//ImagePopup.show(source, href, source.href);
911 			ImagePopup.show(source, href);
912 			return;
913 		} else if (href.match(/nicovideo\.jp/)) {
914 			var iframe = document.createElement("iframe");
915 			iframe.width = 340;
916 			iframe.height = 200;
917 			iframe.src = href;
918 			var nodePopup = Popup.add(iframe, source);
919 			if (nodePopup) {
920 				nodePopup.container.className = "popupIframe";
921 				nodePopup.content.style.maxWidth  = iframe.width  - 7;
922 				nodePopup.content.style.maxHeight = iframe.height - 7;
923 				Popup.reposition(nodePopup);
924 			}
925 		} else {
926 			var obj = document.createElement("object");
927 			obj.setAttribute("type", href.match(/\.divx$/) ? "video/divx" : "application/x-shockwave-flash");
928 			obj.setAttribute("data", href);
929 			obj.setAttribute("width",  "340");
930 			obj.setAttribute("height", "288");
931 			var param = document.createElement("param");
932 			param.setAttribute("name",  "wmode");
933 			param.setAttribute("value", "transparent");
934 			obj.appendChild(param);
935 			var nodePopup = Popup.add(obj, source);
936 			if (nodePopup) {
937 				nodePopup.container.className = "popupVideo";
938 				Popup.reposition(nodePopup);
939 			}
940 		}
941 	}
942 };
943 
944 
945 /**
946  * フッタの「おすすめ2ちゃんねる」のポップアップを管理します。
947  * @static
948  */
949 var Osusume2chPopup = {
950 	/** イベントリスナを登録します。*/
951 	initialise: function() {
952 		window.addEventListener("mouseover", this.onMouseOver.bind(this), false);
953 	},
954 	/** mouseover イベントを処理します。
955 	 * @param  {event} e イベント
956 	 */
957 	onMouseOver: function(e) {
958 		var node = e.target;
959 		if (node.id != "osusume2ch") return;
960 		this.show(node);
961 	},
962 	/** ポップアップを表示します。onMouseOver() から呼ばれます。
963 	 * @param {element} source トリガー元の要素
964 	 */
965 	show: function(source) {
966 		var iframe = document.createElement("iframe");
967 		iframe.width = 500;
968 		iframe.height = 200;
969 		iframe.src = "http://" + ThreadDocument.host + "/" + ThreadDocument.boardName + "/i/" + ThreadDocument.threadID + ".html";
970 		var nodePopup = Popup.add(iframe, source);
971 		if (nodePopup) {
972 			nodePopup.container.className = "popupIframe";
973 			nodePopup.content.style.maxWidth  = iframe.width  - 7;
974 			nodePopup.content.style.maxHeight = iframe.height - 7;
975 			Popup.reposition(nodePopup);
976 		}
977 	}
978 };
979 
980 /**
981  * フッタの「関連キーワード」のポップアップを管理します。
982  * @static
983  */
984 var RelatedKeywordsPopup = {
985 	/** イベントリスナを登録します。*/
986 	initialise: function() {
987 		window.addEventListener("mouseover", this.onMouseOver.bind(this), false);
988 	},
989 	/** mouseover イベントを処理します。
990 	 * @param  {event} e イベント
991 	 */
992 	onMouseOver: function(e) {
993 		var node = e.target;
994 		if (node.id != "relatedKeywords") return;
995 		this.show(node);
996 	},
997 	/** ポップアップを表示します。onMouseOver() から呼ばれます。
998 	 * @param {element} source トリガー元の要素
999 	 */
1000 	show: function(source) {
1001 		var iframe = document.createElement("iframe");
1002 		iframe.width = 400;
1003 		iframe.style.height = "1.5em";
1004 		iframe.src = "http://p2.2ch.io/getf.cgi?" + EXACT_URL;
1005 		var nodePopup = Popup.add(iframe, source);
1006 		if (nodePopup) {
1007 			nodePopup.container.className = "popupIframe";
1008 			nodePopup.content.style.maxWidth  = iframe.offsetWidth  - 7;
1009 			nodePopup.content.style.maxHeight = iframe.offsetHeight - 7;
1010 			Popup.reposition(nodePopup);
1011 		}
1012 	}
1013 };
1014