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