1 // general.js 2 // (2007/02/03) 3 4 /** 5 * @fileOverview メインのスクリプト。ゴタゴタしている。 6 */ 7 8 // excerpt from prototype.js 1.4.1 (but slightly modified) 9 // Copyright (c) 2005-2007 Sam Stephenson 10 /** prototype.js の bind() です。this を束縛します。 */ 11 Function.prototype.bind = function() { 12 var self = this; 13 var args = Array.prototype.slice.call(arguments); 14 var object = args.shift(); 15 return function() { 16 return self.apply(object, args.concat(Array.prototype.slice.call(arguments))); 17 } 18 } 19 20 /** 21 * DOM 操作のために、スキンの静的なノードを格納して管理します。 22 * @static 23 */ 24 var Nodes = { 25 /** ヘッダ 26 * @type element 27 */ 28 header: document.getElementById("header"), 29 /** ヘッダの右側のコマンドバー 30 * @type element 31 */ 32 command: document.getElementById("command"), 33 /** 検索/抽出ボックス 34 * @type element 35 */ 36 findBox: document.getElementById("findbox"), 37 /** 検索/抽出ボックスのテキストボックス 38 * @type element 39 */ 40 findBoxText:document.getElementById("findboxText"), 41 /** レス数 42 * @type element 43 */ 44 resCount: document.getElementById("resCount"), 45 /** ステータス表示 46 * @type element 47 */ 48 statusText: document.getElementById("statusText"), 49 /** スレッドタイトル 50 * @type element 51 */ 52 threadName: document.getElementById("threadName"), 53 /** 板の名称 54 * @type element 55 */ 56 boardName: document.getElementById("boardName"), 57 /** サイドバー<strong>(暫定)</strong> 58 * @type element 59 */ 60 sidePanel: document.getElementById("sidePanel"), 61 /** スクロールビュー<strong>(暫定)</strong> 62 * @type element 63 */ 64 scrollView: document.getElementById("scrollView"), 65 /** 既読レスと新着レスとの境界 66 * @type element 67 */ 68 newMark: document.getElementById("NewMark"), 69 /** レスのコンテナ 70 * @type element 71 */ 72 content: document.getElementById("content"), 73 /** 各メンバを初期化します。*/ 74 initialise: function() { 75 this.header = document.getElementById("header"); 76 this.command = document.getElementById("command"); 77 this.menu = document.getElementById("menu"); 78 this.findBox = document.getElementById("findbox"); 79 this.findBoxText = document.getElementById("findboxText"); 80 this.resCount = document.getElementById("resCount"); 81 this.statusText = document.getElementById("statusText"); 82 this.threadName = document.getElementById("threadName"); 83 this.boardName = document.getElementById("boardName"); 84 this.sidePanel = document.getElementById("sidePanel"); 85 this.scrollView = document.getElementById("scrollView"); 86 this.newMark = document.getElementById("NewMark"); 87 this.content = document.getElementById("content"); 88 }, 89 /** レス要素の配列を取得します。<em>(obsolete)</em> 90 * @return {Array} 配列 91 */ 92 getResItems: function() { 93 return document.getElementsByName("resItems"); 94 }, 95 /** ID 要素の配列を取得します。<em>(obsolete)</em> 96 * @return {Array} 配列 97 */ 98 getIDItems: function() { 99 return document.getElementsByName("idItems"); 100 }, 101 /** 指定のレス番号のレス要素を取得します。 102 * @param {Number} index レス番号 103 * @return {element} 要素 104 */ 105 getRes: function(index) { 106 return document.getElementById("res" + index); 107 }, 108 /** 指定のレス番号の本文要素を取得します。 109 * @param {Number} index レス番号 110 * @return {element} 要素 111 */ 112 getBody: function(index) { 113 return document.getElementById("body" + index); 114 }, 115 /** 指定のレス番号の名前欄要素を取得します。 116 * @param {Number} index レス番号 117 * @return {element} 要素 118 */ 119 getName: function(index) { 120 return document.getElementById("res" + index).childNodes[1].childNodes[3].childNodes[1]; 121 }, 122 /** 指定のレス番号のメール欄要素を取得します。 123 * @param {Number} index レス番号 124 * @return {element} 要素 125 */ 126 getMail: function(index) { 127 return document.getElementById("res" + index).childNodes[1].childNodes[3].childNodes[3]; 128 }, 129 /** 指定のレス番号の日付欄要素を取得します。 130 * @param {Number} index レス番号 131 * @return {element} 要素 132 */ 133 getDate: function(index) { 134 return document.getElementById("res" + index).childNodes[1].childNodes[3].childNodes[5]; 135 }, 136 /** 指定のレス番号の ID 欄要素を取得します。 137 * @param {Number} index レス番号 138 * @return {element} 要素 139 */ 140 getID: function(index) { 141 return document.getElementById("res" + index).childNodes[1].childNodes[3].childNodes[7]; 142 }, 143 /** 指定のレス番号の BeID 欄要素を取得します。 144 * @param {Number} index レス番号 145 * @return {element} 要素 146 */ 147 getBeID: function(index) { 148 return document.getElementById("res" + index).childNodes[1].childNodes[3].childNodes[9]; 149 }, 150 /** 指定のレス要素から、レス番号を取得します。 151 * @param {element} node 要素 152 * @return {Number} レス番号 153 */ 154 getIndexFromRes: function(node) { 155 if (node) return parseInt(node.id.slice(3)); 156 }, 157 /** 指定の本文要素から、レス番号を取得します。 158 * @param {element} node 要素 159 * @return {Number} レス番号 160 */ 161 getIndexFromBody: function(node) { 162 if (node) return parseInt(node.id.slice(4)); 163 } 164 }; 165 166 // XPathItem 167 /** 168 * XPath 式を評価し、その結果を保持します。 169 * @class 170 * @param {String} xPath XPath 式 171 * @param {element} [contextNode] 評価する親ノード 172 * @property {Node} snapshot スナップショット 173 * @property {Node} length マッチしたノードの数 174 * @property {Node} first 最初のノード 175 * @property {Node} last 最後のノード 176 */ 177 function XPathItem() { 178 this.initialise.apply(this, arguments); 179 } 180 XPathItem.prototype = { 181 /** XPath 式 182 * @type String 183 */ 184 xPath: null, 185 /** 評価する親ノード 186 * @type element 187 */ 188 contextNode: null, 189 /** 評価結果 190 * @type XPathResult 191 */ 192 result: null, 193 /** コンストラクタから自動的に呼ばれ、初期化処理を行います。 */ 194 initialise: function(xPath, contextNode) { 195 this.xPath = xPath; 196 this.contextNode = (contextNode) ? contextNode : document; 197 this.result = document.evaluate(this.xPath, this.contextNode, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); 198 }, 199 get snapshot() { 200 return this.result.snapshotItem; 201 }, 202 /** スナップショットから、指定のインデックスのノードを取得します。 203 * @param {Number} index インデックス 204 * @return {element} 要素 205 */ 206 items: function(index) { 207 return this.result.snapshotItem(index); 208 }, 209 get length() { 210 return this.result.snapshotLength; 211 }, 212 get first() { 213 if (this.result.snapshotLength > 0) return this.result.snapshotItem(0); 214 }, 215 get last() { 216 if (this.result.snapshotLength > 0) return this.result.snapshotItem(this.result.snapshotLength - 1); 217 } 218 } 219 220 /** 221 * DOM 操作のために、主に動的なノードを管理します。 222 * @static 223 */ 224 var ResNodes = { 225 /** コンテキストメニューが表示されているかどうかを取得します。 226 * @return {Boolean} 表示されていれば true 227 */ 228 isContextMenuVisible: function() { 229 var result = document.evaluate(".//div[@class='contextMenu']", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); 230 return result.singleNodeValue ? true : false; 231 }, 232 /** レス要素の子要素から、親要素を検索します。 233 * @param {element} contextNode 評価する親ノード 234 * @param {Boolean} [popup] ポップアップ内の要素を含めるかどうか 235 * @return {element} 要素 236 */ 237 getParentContainer: function(contextNode, popup) { 238 var result = document.evaluate(popup ? ".//ancestor::dl" : ".//ancestor::dl[@id][@class='resContainer']", contextNode, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); 239 return result.singleNodeValue; 240 }, 241 /** 指定されたレス要素から、レス番号を取得します。 242 * @param {element} node 要素 243 * @return {Number} レス番号 244 */ 245 getIndexByContainer: function(node) { 246 if (node) return parseInt(node.id.slice(3)); 247 }, 248 /** レス要素の親要素である、レスセレクタ要素からレス番号を取得します。 249 * @param {element} node 要素 250 * @return {Number} レス番号 251 */ 252 getIndexBySelector: function(node) { 253 //if (node) return parseInt(node.firstChild.id.slice(3)); 254 if (node) return parseInt(node.firstChild.id.slice(3)); 255 }, 256 /** レス要素の子要素である、ID 要素からレス番号を取得します。 257 * @param {element} node 要素 258 * @return {Number} レス番号 259 */ 260 getIndexByID: function(node) { 261 if (node) return parseInt(node.parentNode.parentNode.parentNode.id.slice(3)); 262 }, 263 /** レス要素の子要素である、本文要素からレス番号を取得します。 264 * @param {element} node 要素 265 * @return {Number} レス番号 266 */ 267 getIndexByBody: function(node) { 268 if (node) return parseInt(node.id.slice(4)); 269 }, 270 /** 指定されたレス番号から、レス要素を取得します。 271 * @param {Number} index レス番号 272 * @param {element} contextNode 評価する親ノード 273 * @return {XPathItem} XPathItem オブジェクト 274 */ 275 getContainerByIndex: function(index, contextNode) { 276 return new XPathItem(".//dl[@id='res" + index +"']", contextNode).first; 277 }, 278 /** 指定されたレス番号から、本文要素を取得します。 279 * @param {Number} index レス番号 280 * @param {element} contextNode 評価する親ノード 281 * @return {XPathItem} XPathItem オブジェクト 282 */ 283 getBodyByIndex: function(index, contextNode) { 284 return new XPathItem(".//dd[@id='body" + index +"']", contextNode).first; 285 }, 286 /** すべてのレス要素を取得します。 287 * @param {element} contextNode 評価する親ノード 288 * @param {Boolean} [popup] ポップアップ内の要素を含めるかどうか 289 * @return {XPathItem} XPathItem オブジェクト 290 */ 291 getContainers: function(contextNode, popup) { 292 return new XPathItem(popup ? ".//dl" : ".//dl[@id][@class='resContainer']", contextNode); 293 }, 294 /** すべての既読レスヘッダ要素を取得します。 295 * @param {element} contextNode 評価する親ノード 296 * @param {Boolean} [popup] ポップアップ内の要素を含めるかどうか 297 * @return {XPathItem} XPathItem オブジェクト 298 */ 299 getHeaders: function(contextNode) { 300 return new XPathItem(".//dt[@class='resHeader']", contextNode); 301 }, 302 /** すべての新着レスヘッダ要素を取得します。 303 * @param {element} contextNode 評価する親ノード 304 * @param {Boolean} [popup] ポップアップ内の要素を含めるかどうか 305 * @return {XPathItem} XPathItem オブジェクト 306 */ 307 getNewHeaders: function(contextNode) { 308 return new XPathItem(".//dt[@class='resNewHeader']", contextNode); 309 }, 310 /** すべてのレス番号要素を取得します。 311 * @param {element} contextNode 評価する親ノード 312 * @param {Boolean} [popup] ポップアップ内の要素を含めるかどうか 313 * @return {XPathItem} XPathItem オブジェクト 314 */ 315 getNumbers: function(contextNode, popup) { 316 return new XPathItem(popup ? ".//descendant::div[@class='resNumber']/a" : ".//dl[@id][@class='resContainer']/descendant::div[@class='resNumber']/a", contextNode); 317 }, 318 /** すべての非マーク済みレス番号要素を取得します。 319 * @param {element} contextNode 評価する親ノード 320 * @param {Boolean} [popup] ポップアップ内の要素を含めるかどうか 321 * @return {XPathItem} XPathItem オブジェクト 322 */ 323 getUnmarkedNumbers: function(contextNode, popup) { 324 return new XPathItem(popup ? ".//descendant::div[@class='resNumber']/a[not(@class)]" : ".//dl[@id][@class='resContainer']/descendant::div[@class='resNumber']/a[not(@class)]", contextNode); 325 }, 326 /** すべての名前欄要素を取得します。 327 * @param {element} contextNode 評価する親ノード 328 * @param {Boolean} [popup] ポップアップ内の要素を含めるかどうか 329 * @return {XPathItem} XPathItem オブジェクト 330 */ 331 getNames: function(contextNode, popup) { 332 return new XPathItem(popup ? ".//descendant::span[@class='resName']" : ".//dl[@id][@class='resContainer']/descendant::span[@class='resName']", contextNode); 333 }, 334 /** すべてのメール欄要素を取得します。 335 * @param {element} contextNode 評価する親ノード 336 * @param {Boolean} [popup] ポップアップ内の要素を含めるかどうか 337 * @return {XPathItem} XPathItem オブジェクト 338 */ 339 getMails: function(contextNode, popup) { 340 return new XPathItem(popup ? ".//descendant::span[@class='resMail']" : ".//dl[@id][@class='resContainer']/descendant::span[@class='resMail']", contextNode); 341 }, 342 /** すべての日付欄要素を取得します。 343 * @param {element} contextNode 評価する親ノード 344 * @param {Boolean} [popup] ポップアップ内の要素を含めるかどうか 345 * @return {XPathItem} XPathItem オブジェクト 346 */ 347 getDates: function(contextNode, popup) { 348 return new XPathItem(popup ? ".//descendant::span[@class='resDate']" : ".//dl[@id][@class='resContainer']/descendant::span[@class='resDate']", contextNode); 349 }, 350 /** すべての ID 欄要素を取得します。 351 * @param {element} contextNode 評価する親ノード 352 * @param {Boolean} [popup] ポップアップ内の要素を含めるかどうか 353 * @return {XPathItem} XPathItem オブジェクト 354 */ 355 getIDs: function(contextNode, popup) { 356 return new XPathItem(popup ? ".//descendant::span[@rel]" : ".//dl[@id][@class='resContainer']/descendant::span[@rel]", contextNode); 357 }, 358 /** ID として有効な、すべての ID 欄要素を取得します。 359 * @param {element} contextNode 評価する親ノード 360 * @param {Boolean} [popup] ポップアップ内の要素を含めるかどうか 361 * @return {XPathItem} XPathItem オブジェクト 362 */ 363 getValidIDs: function(contextNode, popup) { 364 return new XPathItem(".//dl[@id][@class='resContainer']/descendant::span[string-length(string(@rel)) >= 8]", contextNode); 365 }, 366 /** すべての BeID 欄要素を取得します。 367 * @param {element} contextNode 評価する親ノード 368 * @param {Boolean} [popup] ポップアップ内の要素を含めるかどうか 369 * @return {XPathItem} XPathItem オブジェクト 370 */ 371 getBeIDs: function(contextNode, popup) { 372 return new XPathItem(".//dl[@id][@class='resContainer']/descendant::span[@class='resBeID']", contextNode); 373 }, 374 /** すべての本文要素を取得します。 375 * @param {element} contextNode 評価する親ノード 376 * @param {Boolean} [popup] ポップアップ内の要素を含めるかどうか 377 * @return {XPathItem} XPathItem オブジェクト 378 */ 379 getBodies: function(contextNode, popup) { 380 return new XPathItem(".//dl[@id][@class='resContainer']/descendant::dd[@class='resBody']", contextNode); 381 }, 382 /** すべてのレスアンカー要素を取得します。 383 * @param {element} contextNode 評価する親ノード 384 * @param {Boolean} [popup] ポップアップ内の要素を含めるかどうか 385 * @return {XPathItem} XPathItem オブジェクト 386 */ 387 getResAnchors: function(contextNode, popup) { 388 return new XPathItem(".//dd[@id]/descendant::a[@class='resPointer']", contextNode); 389 }, 390 /** すべての ID アンカー要素を取得します。 391 * @param {element} contextNode 評価する親ノード 392 * @param {Boolean} [popup] ポップアップ内の要素を含めるかどうか 393 * @return {XPathItem} XPathItem オブジェクト 394 */ 395 getIDAnchors: function(contextNode, popup) { 396 return new XPathItem(".//dd[@class='resBody']/descendant::span[starts-with(@class, 'mesID_')]", contextNode); 397 }, 398 /** すべての外部リンク要素を取得します。 399 * @param {element} contextNode 評価する親ノード 400 * @param {Boolean} [popup] ポップアップ内の要素を含めるかどうか 401 * @return {XPathItem} XPathItem オブジェクト 402 */ 403 getOutLinks: function(contextNode, popup) { 404 return new XPathItem(".//dd[@class='resBody']/descendant::a[@class='outLink']", contextNode); 405 }, 406 /** すべてのレスセレクタ要素を取得します。 407 * @param {element} contextNode 評価する親ノード 408 * @return {XPathItem} XPathItem オブジェクト 409 */ 410 getSelectors: function(contextNode) { 411 var obj = new XPathItem(".//div[@class='resUnselected']|.//div[@class='resSelected']", contextNode); 412 obj.selected = new XPathItem(".//div[@class='resSelected']", contextNode); 413 obj.unselected = new XPathItem(".//div[@class='resUnselected']", contextNode); 414 return obj; 415 }, 416 /** Favicon の link 要素を取得します。 417 * @param {element} contextNode 評価する親ノード 418 * @return {XPathItem} XPathItem オブジェクト 419 */ 420 getFavicon: function(contextNode) { 421 return new XPathItem(".//link[@rel='icon']", contextNode).first; 422 }, 423 /** すべてのレス要素の親要素である div 要素を取得します。 424 * @param {element} contextNode 評価する親ノード 425 * @return {XPathItem} XPathItem オブジェクト 426 */ 427 getContent: function(contextNode) { 428 return new XPathItem(".//div[@id='content']", contextNode).first; 429 }, 430 /** レス番号から、指定された形式でレスの内容を取得。 431 * @param {Number} index レス番号 432 * @param {String} format Jane 形式で取得する場合は "Jane" を指定する(デフォルトで 2ch 形式) 433 * @return {String} 文字列 434 */ 435 getEntireTextByIndex: function(index, format) { 436 var container = this.getContainerByIndex(index); 437 if (!container) return null; 438 container = container.parentNode; 439 var number = index; 440 var name = this.getNameText(container); 441 var mail = this.getMailText(container); 442 var date = this.getDateText(container); 443 var id = this.getIDText(container); 444 var beid = this.getBeIDText(container); 445 var body = this.getBodyText(container); 446 var str = ""; 447 if (format == "Jane") { 448 str = number + " 名前:" + name + "[" + mail + "]" + " 投稿日:" + date; 449 } else { 450 str = number + " :" + name + ":" + date; 451 } 452 if (id) str += " " + id; 453 if (beid) str += " " + beid; 454 str += "\n" + body; 455 return str; 456 }, 457 /** 名前欄の文字列を取得します。 458 * @param {element} contextNode 評価する親ノード 459 * @param {Boolean} [popup] ポップアップ内の要素を含めるかどうか 460 * @return {String} 文字列 461 */ 462 getNameText: function(contextNode, popup) { 463 return this.getNames(contextNode, popup).first.textContent; 464 }, 465 /** メール欄の文字列を取得します。 466 * @param {element} contextNode 評価する親ノード 467 * @param {Boolean} [popup] ポップアップ内の要素を含めるかどうか 468 * @return {String} 文字列 469 */ 470 getMailText: function(contextNode, popup) { 471 return this.getMails(contextNode, popup).first.textContent.replace(/^ | $/g, ""); 472 }, 473 /** 日付欄の文字列を取得します。 474 * @param {element} contextNode 評価する親ノード 475 * @param {Boolean} [popup] ポップアップ内の要素を含めるかどうか 476 * @return {String} 文字列 477 */ 478 getDateText: function(contextNode, popup) { 479 return this.getDates(contextNode, popup).first.textContent.replace(/^ | $/g, ""); 480 }, 481 /** ID 欄の文字列を取得します。 482 * @param {element} contextNode 評価する親ノード 483 * @param {Boolean} [popup] ポップアップ内の要素を含めるかどうか 484 * @return {String} 文字列 485 */ 486 getIDText: function(contextNode, popup) { 487 return this.getIDs(contextNode, popup).first.textContent.replace(/^ | $/g, ""); 488 }, 489 /** BeID 欄の文字列を取得します。 490 * @param {element} contextNode 評価する親ノード 491 * @param {Boolean} [popup] ポップアップ内の要素を含めるかどうか 492 * @return {String} 文字列 493 */ 494 getBeIDText: function(contextNode, popup) { 495 return this.getBeIDs(contextNode, popup).first.textContent.replace(/^ | $/g, ""); 496 }, 497 /** 本文の文字列を取得します。 498 * @param {element} contextNode 評価する親ノード 499 * @param {Boolean} [popup] ポップアップ内の要素を含めるかどうか 500 * @return {String} 文字列 501 */ 502 getBodyText: function(contextNode, popup) { 503 return ThreadDocument.getInnerText(this.getBodies(contextNode, popup).first).replace(/ \n /g, "\n").replace(/^ | $/g, ""); 504 } 505 }; 506 507 // ThreadDocument 508 // いろいろ 509 /** 510 * ドキュメント全体を管理します。 511 * @static 512 */ 513 var ThreadDocument = { 514 /** スレッドタイトル 515 * @type String 516 */ 517 title: "", 518 /** ステータス文字列 519 * @type String 520 */ 521 status: "", 522 /** すべてのレスの件数 523 * @type Number 524 */ 525 countAll: null, 526 /** 既読レスの件数 527 * @type Number 528 */ 529 countRead: null, 530 /** 新着レスの件数 531 * @type Number 532 */ 533 countUnread: null, 534 /** サーバのホスト名 535 * @type String 536 */ 537 host: "", 538 /** 板の ID 539 * @type String 540 */ 541 boardName: "", 542 /** スレッド番号 543 * @type String 544 */ 545 threadID: "", 546 /** スレッド表示のオプション文字列 547 * @type String 548 */ 549 option: "", 550 /** 最後に再読み込みを行った日付 551 * @type Date 552 */ 553 date: new Date(), 554 /** Footer.html から呼ばれる関数で、このスキン全体のスクリプトをここで初期化します。 555 * @param {String} statusText >STATUS/< タグで置換された文字列 556 * @param {String} getResCount >GETRESCOUNT/< タグで置換された文字列 557 * @param {String} newResCount >NEWRESCOUNT/< タグで置換された文字列 558 * @param {String} allResCount >ALLRESCOUNT/< タグで置換された文字列 559 */ 560 initialise: function(statusText, getResCount, newResCount, allResCount) { 561 if (location.href.match(/lite=true/)) return; 562 this.title = document.title; 563 this.status = statusText; 564 this.countRead = parseInt(getResCount); 565 this.countUnread = parseInt(newResCount); 566 this.countAll = parseInt(allResCount); 567 this.host = EXACT_URL.replace(/http:\/\/(.+?)\/test\/read\.cgi\/.*/, "$1"); 568 //if (location.href.match(/read\.cgi\/(.*?)\/(.*?)\/(.*)/)) { 569 if (location.href.match(/read\.cgi\/(.*?)\/(.*)\/(.*)/)) { 570 this.boardName = RegExp.$1; 571 this.threadID = RegExp.$2; 572 this.option = RegExp.$3; 573 } 574 Nodes.initialise(); 575 if (location.href.match(/(2ch\.net|bbspink\.com|machi\.to)/)) Nodes.boardName.textContent = boardTable[this.boardName] ? boardTable[this.boardName] : ""; 576 PopupPreventer.initialise(); 577 ResPopup.initialise(); 578 IDPopup.initialise(); 579 TrackbackPopup.initialise(); 580 ImagePopup.initialise(); 581 UrlPopup.initialise(); 582 VideoPopup.initialise(); 583 Osusume2chPopup.initialise(); 584 RelatedKeywordsPopup.initialise(); 585 IDExtract.initialise(); 586 KeyInput.initialise(); 587 AutoReload.initialise(); 588 HistoryManager.initialise(); 589 PageScroller.initialise(); 590 MultipleResSelector.initialise(); 591 b2rAboneHandler.startup(); 592 FxFindHandler.initialise(); 593 NumericScroller.initialise(); 594 this.setStatus(true); 595 if (!Bookmark.initialise()) this.scrollToNewRes(); 596 597 window.setTimeout((function() { 598 this.modifyAnchors(); 599 this.colourIDs(); 600 this.markTrackbackedResNumbers(); 601 }).bind(this), 1); 602 603 if (SkinPref.getBool("enableContract", true)) window.addEventListener("resize", this.contractCaption.bind(this), false); 604 window.addEventListener("contextmenu", ThreadDocument.onContextMenu, false); 605 }, 606 /** contextmenu イベントを処理します。 607 * @param {event} e イベント 608 */ 609 onContextMenu: function(e) { 610 if (e.target.tagName != "SPAN") if (e.target.tagName != "A") return; 611 //if (window.getSelection().toString().length > 0) return; 612 var selection = window.getSelection(); 613 selection.removeAllRanges(); 614 if (e.target.textContent.match(/ID:([^\(\)]{8,})/)) { 615 var range = document.createRange(); 616 range.setStart(e.target.firstChild, 3); 617 range.setEnd(e.target.firstChild, 3 + RegExp.$1.length); 618 selection.addRange(range); 619 } else { 620 window.getSelection().selectAllChildren(e.target); 621 } 622 //e.returnValue = true; 623 }, 624 /** 文字列をクリップボードにコピーします。 625 * @param {String} text 文字列 626 */ 627 setClipboard: function(text) { 628 // 最速インターフェース研究会より: 629 // http://la.ma.la/misc/js/setClip board2.html 630 swf_data = SKIN_PATH + "setClipboard.swf"; 631 var tmp = document.createElement("div"); 632 tmp.innerHTML = [ 633 '<embed src="', swf_data, '"' 634 ,' FlashVars="code=',encodeURIComponent(text),'"' 635 ,' width="0" height="0"' 636 ,'></embed>' 637 ].join(""); 638 with(tmp.style){ 639 position ="absolute"; 640 left = "-10px"; 641 top = "-10px"; 642 visibility = "hidden"; 643 }; 644 document.body.appendChild(tmp); 645 setTimeout(function(){document.body.removeChild(tmp)},1000) 646 return; 647 }, 648 /** 指定されたノードの文字列を、改行を含めて正しく取得します。 649 * @param {element} node ノード 650 * @return {String} 文字列 651 */ 652 getInnerText: function(node) { 653 var nodes = node.childNodes, 654 ret = []; 655 for (var i = 0; i < nodes.length; i++) 656 if (nodes[i].hasChildNodes()) 657 ret.push(this.getInnerText(nodes[i])); 658 else if (nodes[i].tagName == "BR") 659 ret.push("\n"); 660 else if (nodes[i].nodeType == Node.TEXT_NODE) 661 ret.push(nodes[i].nodeValue); 662 else if (nodes[i].alt) 663 ret.push(nodes[i].alt); 664 return ret.join(''); 665 }, 666 /** スレッドの情報をクリップボードにコピーします。*/ 667 copyThreadInfo: function() { 668 this.setClipboard(this.title + "\n" + EXACT_URL + "\n"); 669 }, 670 /** 指定されたレス番号に対して返信します。 671 * @param {Number} number レス番号 672 */ 673 writeTo: function(number) { 674 location.href = "bbs2ch:post:" + EXACT_URL + number; 675 }, 676 /** すべての新着レスを既読状態にします。*/ 677 markAsRead: function() { 678 var headers = ResNodes.getNewHeaders(); 679 for (var i = 0; i < headers.length; i++) 680 headers.items(i).className = "resHeader"; 681 }, 682 /** 新着レスまでスクロールします。*/ 683 scrollToNewRes: function(){ 684 if(document.location.href.indexOf("#") != -1) return; 685 if (this.option.match(/^\d+/)) { // we're in log pick-up mode 686 window.scrollTo(0, 0); 687 return; 688 } 689 if (Nodes.newMark) window.scrollTo(0, Nodes.newMark.offsetTop - (Nodes.header.offsetTop + Nodes.header.offsetHeight)); 690 }, 691 /** Favicon を設定します。 692 * @param {String} filename アイコンのファイル名 693 * @param {Boolean} inactive 非アクティヴ状態かどうか 694 */ 695 setFavicon: function(filename, inactive) { 696 var favicon = ResNodes.getFavicon(); 697 if (favicon) { 698 var link = favicon.cloneNode(true); 699 if (filename == "throbber") { 700 link.href = SKIN_PATH + "img/favicon/favicon" + filename + ".gif"; 701 } else { 702 if (AutoReload.enabled) { 703 link.href = SKIN_PATH + "img/favicon/favicon" + filename + "auto" + (inactive ? "inactive" : "") + ".png"; 704 } else { 705 link.href = SKIN_PATH + "img/favicon/favicon" + filename+ ".png"; 706 } 707 } 708 favicon.parentNode.appendChild(link); 709 favicon.parentNode.removeChild(favicon); 710 } 711 }, 712 /** ステータスを設定して、表示を更新します。 713 * @param {String} updateStatus ステータス文字列 714 */ 715 setStatus: function(updateStatus) { 716 /* 717 ok = (`・ω・´)「OK」 718 dat_down = ( ーωー)「DAT 落ち」 719 not_modified = ( ーωー)「新着なし」 720 abone = (´・ω・`)「あぼーん発生。右下のバッテンを押してログを消した後再読み込みしてね」 721 error = (´・ω・`)「エラー : %S」 722 723 offline_mode = ( ーωー)「オフラインモード」 724 log_pickup_mode = ( ーωー)「ログピックアップモード」 725 */ 726 Nodes.resCount.textContent = "(" + this.countAll + ")"; 727 if (updateStatus) { 728 //if (this.countUnread > 0) { 729 if ((this.countUnread > 0) && (this.status != "( ーωー)「ふー」")) { 730 Nodes.statusText.className = "ok"; 731 this.status = this.countUnread + "件の新着レス"; 732 this.setFavicon("new"); 733 } else { 734 switch(this.status){ 735 case "( ーωー)「DAT 落ち」": 736 Nodes.statusText.className = "warning"; 737 this.status = "DAT 落ち"; 738 this.setFavicon("warning"); 739 break; 740 case "(`・ω・´)「OK」": // for shitaraba 741 case "( ーωー)「新着なし」": 742 Nodes.statusText.className = "ok"; 743 this.status = "新着なし"; 744 this.setFavicon(""); 745 break; 746 case "(´・ω・`)「あぼーん発生。右下のバッテンを押してログを消した後再読み込みしてね」": 747 Nodes.statusText.className = "warning"; 748 this.status = "あぼーんが発生しました。ログを消して、再読み込みして下さい。"; 749 this.setFavicon("warning"); 750 break; 751 case "( ーωー)「オフラインモード」": 752 Nodes.statusText.className = "warning"; 753 this.status = "オフラインモード"; 754 this.setFavicon("warning"); 755 break; 756 case "( ーωー)「ログピックアップモード」": 757 Nodes.statusText.className = "warning"; 758 this.status = "ログピックアップモード"; 759 this.setFavicon("warning"); 760 break; 761 case "( ーωー)「ふー」": 762 Nodes.statusText.className = "warning"; 763 var now = new Date(); 764 this.status = "リロード制限(残り" + Math.ceil((5000 - (now.getTime() - this.date.getTime())) / 1000) + "秒)"; 765 this.setFavicon("warning"); 766 break; 767 default: 768 Nodes.statusText.className = "error"; 769 if (this.status.match("\(´・ω・`\)「エラー : (.+)」")) this.status = RegExp.$1; 770 this.setFavicon("error"); 771 } 772 } 773 } 774 //if (updateStatus) this.status = "+" + this.countUnread + ""; 775 Nodes.statusText.textContent = this.status; 776 this.contractCaption(); 777 }, 778 /** スレッドタイトルのフォントにおける、指定された文字列の幅を取得します。 779 * @param {String} str 文字列 780 * @return {Number} 幅 781 */ 782 getCaptionWidth: function(str) { 783 var span = document.createElement("span"); 784 span.visibility = "hidden"; 785 span.appendChild(document.createTextNode(str)); 786 Nodes.header.appendChild(span); 787 var width = span.offsetWidth; 788 Nodes.header.removeChild(span); 789 return width; 790 }, 791 /** スレッドタイトルを省略表示します。*/ 792 contractCaption: function() { 793 var statusWidth = (Nodes.statusText.offsetLeft + Nodes.statusText.offsetWidth) - (Nodes.threadName.offsetWidth); 794 var dotWidth = this.getCaptionWidth("..."); 795 if (this.getCaptionWidth(ThreadDocument.title) + statusWidth > Nodes.command.offsetLeft) { 796 for (var i = 1; i < this.title.length; i++) { 797 if ((this.getCaptionWidth(this.title.substring(0,i)) + dotWidth + statusWidth) > Nodes.command.offsetLeft) { 798 Nodes.threadName.textContent = this.title.substring(0,i-1) + "..."; 799 break; 800 } 801 } 802 } else { 803 Nodes.threadName.textContent = this.title; 804 } 805 }, 806 /** ID 欄を色分けし、発言回数を末端に追加します。*/ 807 colourIDs: function() { 808 ID.traverse(); 809 //var idItems = ResNodes.getValidIDItems(); 810 var idItems = ResNodes.getValidIDs(); 811 var f = IDExtract.extract.bind(IDExtract); 812 for (var i = 0; i < idItems.length; i++) { 813 var node = idItems.items(i); 814 var id = node.getAttribute("rel"); 815 var len = ID.items[id].length; 816 if (len > 1) { 817 node.textContent = "ID:" + id+ "(" + len + ")"; 818 node.className = (len > 2) ? "resID2" : "resID1"; 819 node.onclick = f; 820 } 821 } 822 //var idAnchorItems = ResNodes.getIDAnchorItems(); 823 var idAnchorItems = ResNodes.getIDAnchors(); 824 for (var i = 0; i < idAnchorItems.length; i++) { 825 var node = idAnchorItems.items(i); 826 var pos = node.className.indexOf(" "); 827 var className = (pos == -1) ? node.className : node.className.slice(0, pos); 828 var id = className.slice(6); 829 if (ID.items[id]) { 830 var len = ID.items[id].length; 831 node.className = className + ((len > 2) ? " resID2" : " resID1"); 832 node.onclick = f; 833 } 834 } 835 }, 836 /** 被参照レスに下線を引き、その下に件数を表示します。*/ 837 markTrackbackedResNumbers: function() { 838 Trackback.traverse(); 839 var numbers = ResNodes.getUnmarkedNumbers(); 840 var div = document.createElement("div"); 841 div.className = "count"; 842 for (var i = 0; i < numbers.length; i++) { 843 var node = numbers.items(i); 844 var tb = Trackback.items[node.textContent]; 845 if (tb) { 846 node.className = "trackback"; 847 var count = div.cloneNode(false); 848 count.textContent = tb.length; 849 node.parentNode.appendChild(count); 850 } 851 } 852 }, 853 /** 設定に応じて、アンカーの属性を変更します。 854 * @param {element} contextNode 評価する親ノード 855 */ 856 modifyAnchors: function(contextNode) { 857 var enableNewWindow = SkinPref.getBool("enableNewWindow", true); 858 var enableNoReferer = SkinPref.getBool("enableNoReferer", true); 859 var outLinkItems = ResNodes.getOutLinks(contextNode); 860 for (var i = 0; i < outLinkItems.length; i++) { 861 var node = outLinkItems.items(i); 862 if (!node.rel) node.rel = node.href.replace(SERVER_URL, ""); 863 if (enableNewWindow) if (node.target != "_blank") node.target = "_blank"; 864 if ((node.offsetLeft + node.offsetWidth) > document.body.clientWidth) 865 node.innerHTML = (node.textContent.split("")).join("<wbr/>"); 866 if (node.href.indexOf("read.cgi") != -1) 867 if (node.href.indexOf(SERVER_URL) == -1) { 868 node.href = SERVER_URL + node.href; 869 continue; 870 } 871 if (enableNoReferer) if (!ImagePopup.isImage(node.href)) { 872 if (node.href.indexOf("ime.nu.html?url=") == -1) { 873 if (node.textContent.indexOf("xn--") == -1) { 874 node.href = SKIN_PATH + "ime.nu.html?url=" + node.href; 875 } else { 876 node.href = SKIN_PATH + "ime.nu.html?url=" + node.href + "&b64url=" + btoa(unescape(encodeURIComponent(node.href))); 877 } 878 } 879 } 880 } 881 882 var f = this.jumpTo.bind(this); 883 var resAnchorItems = ResNodes.getResAnchors(contextNode); 884 for (var i = 0; i < resAnchorItems.length; i++) { 885 var node = resAnchorItems.items(i); 886 node.href = "javascript:void(0)"; 887 node.onclick = f; 888 } 889 }, 890 /** XMLHttpRequest を利用して、動的にレスを挿入します。 891 * @param {Number} start 挿入するレス番号の始点 892 * @param {Number} end 挿入するレス番号の終点 893 * @param {Boolean} before レスを末端ではなく先頭に追加するかどうか 894 * @param {Boolean } scrollToTop 挿入後に先頭までスクロールさせるかどうか 895 * @param {Function} func 挿入後に実行する関数 896 */ 897 asyncInsert: function(start, end, before, scrollToTop, func) { 898 var xmlHTTP = new XMLHttpRequest(); 899 xmlHTTP.onreadystatechange = xmlEventHandler.bind(this); 900 function xmlEventHandler() { 901 if (xmlHTTP.readyState == 4) { 902 if (xmlHTTP.status == 200) { 903 var divTemp = document.createElement("div"); 904 divTemp.innerHTML = xmlHTTP.responseText; 905 var content = ResNodes.getContent(divTemp); 906 content.id = ""; 907 var resItems = ResNodes.getSelectors(); 908 var resStart = ResNodes.getContainerByIndex(1); 909 resStart = (resStart) ? resItems.items(1) : resItems.items(0); 910 if (resStart.parentNode.id.length == 0) resStart = resStart.parentNode; 911 if (before) { 912 Nodes.content.insertBefore(content, resStart); 913 } else { 914 Nodes.content.appendChild(content); 915 } 916 if (scrollToTop) { 917 window.scrollTo(0,0); 918 } else { 919 var resStart = ResNodes.getContainerByIndex(start); 920 if (resStart) window.scrollTo(0, resStart.offsetTop - (Nodes.header.offsetTop + Nodes.header.offsetHeight)); 921 } 922 923 window.setTimeout((function() { 924 this.modifyAnchors(content); 925 this.colourIDs(); 926 this.markTrackbackedResNumbers(); 927 }).bind(this), 1); 928 929 this.flagDigest = false; 930 if (func) func(); 931 Nodes.statusText.className = "ok"; 932 this.setFavicon(""); 933 } 934 } 935 } 936 Nodes.statusText.className = "throbber"; 937 this.setFavicon("throbber"); 938 xmlHTTP.open("GET", SERVER_URL + EXACT_URL + start + "-" + end + "n" + "?lite=true", true); 939 xmlHTTP.overrideMimeType('text/xml'); 940 xmlHTTP.send(null); 941 }, 942 /** XMLHttpRequest を利用して、表示域外のレスをすべて表示します。*/ 943 showAll: function(){ 944 var nodes = ResNodes.getSelectors(); 945 for(var i = 0; i < nodes.length; i++) nodes.items(i).parentNode.style.display = "block"; 946 ThreadDocument.flagDigest = false; 947 948 var resStart = ResNodes.getContainerByIndex(1); 949 var addFirst = (resStart) ? false : true; 950 resStart = (resStart) ? nodes.items(1) : nodes.items(0); 951 var startIndex = ResNodes.getIndexBySelector(resStart); 952 var endIndex = ResNodes.getIndexBySelector(nodes.items(nodes.length - 1)); 953 //var endIndex = Nodes.getIndexFromRes(resItems[resItems.length-1]); 954 //if (!Nodes.getRes(1)) this.asyncInsert(1, 1, true, true); 955 if (addFirst) this.asyncInsert(1, 1, true, true); 956 if (startIndex > 2) this.asyncInsert(2, startIndex - 1, true, true); 957 if (endIndex < this.countAll) this.asyncInsert(endIndex + 1, this.countAll, false, true); 958 }, 959 /** XMLHttpRequest を利用して、更新をチェックし新着レスがあれば挿入します。 960 * @param {Boolean} noScroll 挿入後にスクロールしないかどうか 961 */ 962 reload: function(noScroll){ 963 if (this.countAll >= 1000) if (EXACT_URL.indexOf("2ch.net") != -1) return; 964 var now = new Date(); 965 if ((now.getTime() - this.date.getTime()) < 5000) { 966 this.status = "( ーωー)「ふー」"; 967 this.setStatus(true); 968 return; 969 } 970 Nodes.statusText.className = "throbber"; 971 this.setFavicon("throbber"); 972 this.date = now; 973 this.markAsRead(); 974 var xmlHTTP = new XMLHttpRequest(); 975 xmlHTTP.onreadystatechange = xmlEventHandler.bind(this); 976 function xmlEventHandler() { 977 if (xmlHTTP.readyState == 4) { 978 if (xmlHTTP.status == 200) { 979 var divTemp = document.createElement("div"); 980 divTemp.innerHTML = xmlHTTP.responseText; 981 982 var content = ResNodes.getContent(divTemp); 983 content.id = ""; 984 var resItems = ResNodes.getContainers(divTemp); 985 // 更新がなければ最後のレスだけ帰ってくる 986 if (resItems.length <= 1) { 987 this.status = "( ーωー)「新着なし」"; 988 this.countUnread = 0; 989 this.countRead = this.countAll; 990 this.setStatus(true); 991 return; 992 } 993 // 一個余分についてきてしまうので、消す 994 var firstItem = ResNodes.getSelectors(content).first; 995 if (firstItem) content.removeChild(firstItem); 996 997 this.countRead = this.countAll; 998 //this.countUnread = resItems.length ; 999 this.countUnread = resItems.length - 1; 1000 this.countAll = this.countRead + this.countUnread; 1001 this.setStatus(true); 1002 Nodes.content.appendChild(content); 1003 1004 window.setTimeout((function() { 1005 this.modifyAnchors(content); 1006 this.colourIDs(); 1007 this.markTrackbackedResNumbers(); 1008 }).bind(this), 1); 1009 1010 if (!noScroll) { 1011 var resStart = ResNodes.getSelectors(content).first; 1012 if (resStart) window.scrollTo(0, resStart.offsetTop - (Nodes.header.offsetTop + Nodes.header.offsetHeight)); 1013 } 1014 this.flagDigest = false; 1015 } 1016 } 1017 } 1018 xmlHTTP.open("GET", SERVER_URL + EXACT_URL + (ThreadDocument.countAll + 1) + "-" + "?lite=true", true); 1019 xmlHTTP.send(null); 1020 }, 1021 /** 全角文字で表記された数字を、数値型に変換します。 1022 * @param {String} str 全角数字が含まれた文字列 1023 * @return {Number} 変換された数値 1024 */ 1025 toNarrow: function(str) { 1026 return parseInt(str.replace(/([0-9])/g, function(n){ return String.fromCharCode(n.charCodeAt(0) - 0xFEE0); })); 1027 }, 1028 /** 指定されたレスまでスクロールします。アンカーの click イベントから呼ばれます。 1029 * @param {event} e イベント 1030 */ 1031 jumpTo: function(e){ 1032 var node = e.target; 1033 var expPointerAnchor = new RegExp(/(\d{1,4})-?(\d{0,4})/); 1034 var expNameAnchor = new RegExp(/^(\d{1,4})-?(\d{0,4})$/); 1035 switch (node.className) { 1036 case "resPointer": 1037 if (node.textContent.match(expPointerAnchor)) { 1038 var start = this.toNarrow(RegExp.$1); 1039 var end = RegExp.$2 ? this.toNarrow(RegExp.$2) : start; 1040 } 1041 break; 1042 case "resName": 1043 if (node.textContent.match(expNameAnchor)) { 1044 var start = this.toNarrow(RegExp.$1); 1045 var end = RegExp.$2 ? this.toNarrow(RegExp.$2) : start; 1046 } 1047 break; 1048 default: 1049 return; 1050 } 1051 var resTarget = Nodes.getRes(start); 1052 if (resTarget) { 1053 var newStart = 0; 1054 for(var i = start + 1; i <= end; i++) { 1055 if (!this.getRes(i)) { 1056 newStart = i; 1057 break; 1058 } 1059 } 1060 if (newStart == 0) { 1061 window.scrollTo(0, resTarget.offsetTop - (Nodes.header.offsetTop + Nodes.header.offsetHeight)); 1062 return; 1063 } 1064 start = newStart; 1065 } 1066 var resItems = Nodes.getResItems(); 1067 var resStart = Nodes.getRes(1); 1068 resStart = (resStart) ? resItems[1] : resItems[0]; 1069 var startIndex = Nodes.getIndexFromRes(resStart); 1070 var endIndex = Nodes.getIndexFromRes(resItems[resItems.length-1]); 1071 if (start < startIndex) this.asyncInsert(start, startIndex - 1, true, false); 1072 if (endIndex < end) this.asyncInsert(endIndex, end, true, false); 1073 } 1074 }; 1075 1076 /** 1077 * ID を走査して、レス番号と ID の対応テーブルを管理します。 1078 * @static 1079 */ 1080 var ID = { 1081 /** レス番号と ID の対応テーブル 1082 * @type Object 1083 * @example 1084 * { 1085 * "hogehoge": [1, 3, 5], 1086 * "foobar": [200, 201, 208] 1087 * } 1088 */ 1089 items: [], 1090 /** 有効な ID の総個数 1091 * @type Number 1092 */ 1093 count: null, 1094 /** テーブルに項目を追加します。 1095 * @param {String} id ID 1096 * @param {Number} index レス番号 1097 */ 1098 add: function(id, index) { 1099 if (!this.items[id]) this.items[id] = []; 1100 this.items[id].push(index); 1101 }, 1102 /** ID を走査して、対応テーブルを作ります。*/ 1103 traverse: function() { 1104 //var resItems = ResNodes.getResItems(); 1105 //var resItems = ResNodes.getContainers(); 1106 var idItems = ResNodes.getValidIDs(); 1107 //if (resItems.length == this.count) return; 1108 if (idItems.length == this.count) return; 1109 this.count = idItems.length; 1110 this.items = []; 1111 for (var i = 0; i < idItems.length; i++) { 1112 this.add(idItems.items(i).getAttribute("rel"), ResNodes.getIndexByID(idItems.items(i))); 1113 } 1114 } 1115 } 1116 1117 /** 1118 * レスアンカーを走査して、レス番号とレス元の番号との対応テーブルを管理します。 1119 * @static 1120 */ 1121 var Trackback = { 1122 /** レス番号とレス元の番号との対応テーブル 1123 * @type Object 1124 * @example 1125 * { 1126 * "1": [2, 3, 4, 5], 1127 * "4": [5], 1128 * "6": [8] 1129 * } 1130 */ 1131 items: [], 1132 /** 走査したレスの件数 1133 * @type Number 1134 */ 1135 count: null, 1136 /** 全角数字を半角数字に変換するテーブル 1137 * @type Number 1138 */ 1139 narrowTable: {"1":"1","2":"2","3":"3","4":"4","5":"5","6":"6","7":"7","8":"8","9":"9","0":"0"}, 1140 /** 全角文字で表記された数字を、数値型に変換します。 1141 * @param {String} str 全角数字が含まれた文字列 1142 * @return {Number} 変換された数値 1143 */ 1144 toNarrow: function(str) { 1145 //return parseInt(str.replace(/([0-9])/g, function(n){ return String.fromCharCode(n.charCodeAt(0) - 0xFEE0); })); 1146 var array = str.split(""); 1147 for (var i = 0; i < array.length; i++) str[i] = this.narrowTable[str]; 1148 return parseInt(array.join("")); 1149 }, 1150 /** テーブルに項目を追加します。 1151 * @param {Number} destIndex レス先のレス番号 1152 * @param {Number} srcIndex レス元のレス番号 1153 */ 1154 add: function(destIndex, srcIndex) { 1155 if (this.items[destIndex]) { 1156 for (var i = 0; i < this.items[destIndex].length; i++) if (this.items[destIndex][i] == srcIndex) return; 1157 } else { 1158 this.items[destIndex] = []; 1159 } 1160 this.items[destIndex].push(srcIndex); 1161 }, 1162 /** 指定のレス番号についたレスの数を取得します。 1163 * @param {Number} index レス番号 1164 * @return {Number} レスの数 1165 */ 1166 getCount: function(index) { 1167 if (!this.items[index]) return 0; 1168 return this.items[index].length; 1169 }, 1170 /** 指定のレス番号についたレスについて、レスアンカーのリストを生成して返します。 1171 * @param {Number} index レス番号 1172 * @return {DocumentFragment} 要素 1173 */ 1174 getAnchorElements: function(index) { 1175 if (!this.items[index]) return; 1176 var content = document.createDocumentFragment(); 1177 var a = document.createElement("a"); 1178 a.className = "resPointer"; 1179 a.href = "javascript:void(0)"; 1180 a.onclick = (function(e) { ThreadDocument.jumpTo(e) }).bind(this); 1181 for (var i = 0; i < this.items[index].length; i++){ 1182 var item = a.cloneNode(false); 1183 item.appendChild(document.createTextNode(">>" + this.items[index][i])); 1184 content.appendChild(item); 1185 } 1186 return content; 1187 }, 1188 /** 指定の要素に、逆参照情報を追加します。 1189 * @param {element} content 要素 1190 * @param {Number} index レス番号 1191 */ 1192 appendTo: function(content, index) { 1193 if (!this.items[index]) return; 1194 var span = document.createElement("span"); 1195 span.className = "trackBack"; 1196 span.appendChild(document.createTextNode("(参照:")); 1197 span.appendChild(this.getAnchorElements(index)); 1198 span.appendChild(document.createTextNode(")")); 1199 content.appendChild(span); 1200 }, 1201 // XPath で分岐を減らして多少高速化しますた(2007/08/30) 1202 /** レスを走査して、対応テーブルを作ります。*/ 1203 traverse: function() { 1204 const TRACKBACK_LIMIT = 20; 1205 //var r = Nodes.getResItems(); 1206 var r = ResNodes.getContainers(); 1207 if (r.length == this.count) return; 1208 this.count = r.length; 1209 var expAnchor = new RegExp(/(\d{1,4})-?(\d{0,4})/); 1210 this.items = new Array(); 1211 //var anchors = ResNodes.getResAnchorItems(); 1212 var anchors = ResNodes.getResAnchors(); 1213 for (var i = 0; i < anchors.length; i++) { 1214 if (anchors.items(i).textContent.match(expAnchor)) { 1215 var start = this.toNarrow(RegExp.$1); 1216 var end = RegExp.$2 ? this.toNarrow(RegExp.$2) : start; 1217 if ((end - start) < TRACKBACK_LIMIT) { 1218 for(var destIndex = start; destIndex <= end; destIndex++) { 1219 var srcIndex = anchors.items(i).parentNode.id.slice(4); // body 1220 if (srcIndex) this.add(destIndex, srcIndex); 1221 } 1222 } 1223 } 1224 } 1225 } 1226 } 1227 1228 /** 1229 * ID の抽出機能を管理します。 1230 * @static 1231 */ 1232 var IDExtract = { 1233 /** 抽出中の ID (obsolete) 1234 * @type String 1235 */ 1236 id: "", 1237 /** スレッド表示のオプションで抽出が要求された場合、指定された ID を持つレスのみを表示します。(obsolete)*/ 1238 initialise: function() { 1239 if (!location.href.match(/\?id=/)) return; 1240 id = RegExp.rightContext; 1241 if (id.length < 4) return; 1242 if (id.match(/^ID:\?\?\?/)) return; 1243 var resItems = Nodes.getResItems(); 1244 var idItems = Nodes.getIDItems(); 1245 var idCount = 0; 1246 Trackback.traverse(); 1247 for (var i = 0; i < resItems.length; i++) { 1248 if (idItems[i].textContent.indexOf(id,0) != -1){ 1249 Trackback.appendTo(resItems[i], Nodes.getIndexFromRes(resItems[i])); 1250 resItems[i].style.display = "block"; 1251 idCount++; 1252 } else { 1253 resItems[i].style.display = "none"; 1254 } 1255 } 1256 ThreadDocument.status = id + " のレス (" + idCount + "回)"; 1257 ThreadDocument.setStatus(); 1258 IDExtract.id = id; 1259 }, 1260 /** 指定された ID を抽出します。アンカーの click イベントから呼ばれます。 1261 * @param {event} e イベント 1262 */ 1263 extract: function(e) { 1264 if (SkinPref.getBool("enableIDPopupOnClick", false) && !e.ctrlKey) return; 1265 if (!FindBox.isVisible) FindBox.show(); 1266 Nodes.findBoxText.value = "id:" + e.target.getAttribute("rel"); 1267 FindBox.find(); 1268 } 1269 }; 1270 1271 /** 1272 * ショートカットキーを管理します。 1273 * @static 1274 */ 1275 var KeyInput = { 1276 /** イベントリスナを登録します。*/ 1277 initialise: function() { 1278 window.addEventListener("keydown", this.onKeyDown.bind(this), false); 1279 }, 1280 /** keydown イベントを処理します。 1281 * @param {event} e イベント 1282 */ 1283 onKeyDown: function(e) { 1284 this.reloadProc(e); 1285 this.showAllProc(e); 1286 this.writeProc(e); 1287 if (e.keyCode == 70) if (e.altKey && e.ctrlKey) FindBox.show(); 1288 }, 1289 /** [書き込みウィザード] を開く処理を定義します。 1290 * @param {event} e イベント 1291 */ 1292 writeProc: function(e){ 1293 if (e.ctrlKey) if (e.keyCode == 13) { 1294 location.href = "bbs2ch:post:" + EXACT_URL; 1295 e.preventDefault(); 1296 } 1297 }, 1298 /** [すべて表示] を実行する処理を定義します。 1299 * @param {event} e イベント 1300 */ 1301 showAllProc: function(e){ 1302 if (e.ctrlKey) if (e.keyCode == 229 || e.keyCode == 32) { 1303 ThreadDocument.showAll(); 1304 e.preventDefault(); 1305 } 1306 }, 1307 /** 更新を実行する処理を定義します。 1308 * @param {event} e イベント 1309 */ 1310 reloadProc: function(e){ 1311 if (!SkinPref.getBool("enableHookReload", true)) return; 1312 if (e.keyCode == 116) { 1313 ThreadDocument.reload(); 1314 e.preventDefault(); 1315 } 1316 } 1317 }; 1318 1319 /** 1320 * [検索/抽出] 機能を管理します。 1321 * @static 1322 * @property {Boolean} isVisible 検索/抽出ボックスが表示されているかどうか 1323 */ 1324 var FindBox = { 1325 /** 既に初期化済みかどうか 1326 * @type Boolean 1327 */ 1328 initialised: false, 1329 /** 検索/抽出実行前のスクロール位置 1330 * @type Number 1331 */ 1332 scrollY: -1, 1333 get isVisible() { 1334 return (Nodes.findBox.style.display == "block"); 1335 }, 1336 /** keydown イベントを処理します。 1337 * @param {event} e イベント 1338 */ 1339 onKeyPress: function(e) { 1340 switch (e.keyCode) { 1341 case 13: 1342 if (e.target == Nodes.findBoxText) this.find(); 1343 break; 1344 case 27: //ESC 1345 if (this.isVisible) this.clear(); 1346 break; 1347 } 1348 }, 1349 /** click イベントを処理します。 1350 * @param {event} e イベント 1351 */ 1352 onClick: function(e) { 1353 if (e.target.id == "findboxClear") this.clear(); 1354 }, 1355 /** mouesdown イベントを処理します。 1356 * @param {event} e イベント 1357 */ 1358 onMouseDown: function(e) { 1359 if (!e.target.id.match(/(find|findbox|findboxClear|findboxText)/)) 1360 if (!Nodes.findBoxText.value.length) this.hide(); 1361 }, 1362 /** 検索/抽出ボックスを表示します。 1363 * @param {event} e イベント 1364 */ 1365 show: function() { 1366 if (Nodes.findBox.style.display != "block") { 1367 Nodes.findBox.style.display = "block"; 1368 Nodes.findBoxText.focus(); 1369 } else { 1370 this.hide(); 1371 } 1372 if (!this.initialised) { 1373 this.initialised = true; 1374 window.addEventListener("click", this.onClick.bind(this), false); 1375 window.addEventListener("mousedown", this.onMouseDown.bind(this), false); 1376 window.addEventListener("keypress", this.onKeyPress.bind(this), false); 1377 } 1378 }, 1379 /** 検索/抽出ボックスを非表示します。 1380 * @param {event} e イベント 1381 */ 1382 hide: function() { 1383 Nodes.findBox.style.display = "none"; 1384 }, 1385 /** 検索/抽出ボックスをクリアします。 1386 * @param {event} e イベント 1387 */ 1388 clear: function() { 1389 Nodes.findBoxText.value = ""; 1390 Nodes.findBoxText.className = ""; 1391 if (SkinPref.getBool("enableFindHighlight", false)) window.setTimeout(this.unhighlight, 1); 1392 var items = Nodes.getResItems(); 1393 for (var i = 0; i < items.length; i++) { 1394 items[i].style.display = "block"; 1395 } 1396 if (this.scrollY != -1) window.scrollTo(0, this.scrollY); 1397 this.scrollY = -1; 1398 }, 1399 /** 正規表現に使用される文字列をエスケープします。 1400 * @param {String} str 文字列 1401 * @return {String} エスケープされた文字列 1402 */ 1403 escapeExpression: function(str) { 1404 return str.replace(/([.*+?^${}()|[\]\/\\])/g, '\\$1'); 1405 }, 1406 /** 指定された文字列を強調表示します。 1407 * @param {String} str 文字列 1408 */ 1409 highlight: function(str) { 1410 console.time("highlight"); 1411 var x = window.scrollX; 1412 var y = window.scrollY; 1413 var strong = document.createElement("strong"); 1414 while (window.find(str)) { 1415 var range = window.getSelection().getRangeAt(0); 1416 window.setTimeout(function(r){ r.surroundContents(strong.cloneNode(false)); }, 1, range); 1417 } 1418 window.getSelection().removeAllRanges(); 1419 window.scrollTo(x, y); 1420 console.timeEnd("highlight"); 1421 }, 1422 /** 指定された文字列の強調表示を解除します。 1423 * @param {String} str 文字列 1424 */ 1425 unhighlight: function(str) { 1426 console.time("unhighlight"); 1427 var result = document.evaluate('//strong', document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); 1428 for (var i = 0; i < result.snapshotLength; i++) { 1429 var node = result.snapshotItem(i); 1430 window.setTimeout(function(n){ 1431 var doc = document.createDocumentFragment(); 1432 for (var i = 0; i < n.childNodes.length; i++) { 1433 doc.appendChild(n.childNodes[i]); 1434 } 1435 n.parentNode.replaceChild(doc, n); 1436 }, 1, node); 1437 } 1438 console.timeEnd("unhighlight"); 1439 }, 1440 /** 検索/抽出を実行します。*/ 1441 find: function() { 1442 var scrollY = window.scrollY; 1443 var items = Nodes.getResItems(); 1444 var isFound = false; 1445 1446 var xpathResItems = ResNodes.getContainers(); 1447 var xpaths = { "name": ResNodes.getNames, 1448 "mail": ResNodes.getMails, 1449 "date": ResNodes.getDates, 1450 "id": ResNodes.getIDs, 1451 "beid": ResNodes.getBeIDs, 1452 "body": ResNodes.getBodies}; 1453 if (Nodes.findBoxText.value.match(/^(name|mail|date|id|body|related):(.*)/i)) { 1454 var verb = RegExp.$1.toLowerCase(); 1455 switch (verb) { 1456 case "related": 1457 break; 1458 case "id": 1459 var xpath = xpaths["id"](); 1460 for (var i = 0; i < xpath.length; i++) { 1461 var id = xpath.items(i).getAttribute("rel"); 1462 if ((id.length >= 8) && (id.indexOf(RegExp.$2) != -1)) { 1463 isFound = true; 1464 xpathResItems.items(i).style.display = "block"; 1465 } else { 1466 xpathResItems.items(i).style.display = "none"; 1467 } 1468 } 1469 break; 1470 default: 1471 var xpath = xpaths[verb](); 1472 for (var i = 0; i < xpath.length; i++) 1473 { 1474 if ((xpath.items(i).textContent).indexOf(RegExp.$2) != -1) { 1475 isFound = true; 1476 xpathResItems.items(i).style.display = "block"; 1477 } else { 1478 xpathResItems.items(i).style.display = "none"; 1479 } 1480 } 1481 break; 1482 } 1483 } else { 1484 for (var i = 0; i < xpathResItems.length; i++) { 1485 var re = xpathResItems.items(i).textContent.match(this.escapeExpression(Nodes.findBoxText.value), "i"); 1486 if (re) { 1487 isFound = true; 1488 xpathResItems.items(i).style.display = "block"; 1489 } else { 1490 xpathResItems.items(i).style.display = "none"; 1491 } 1492 } 1493 } 1494 Nodes.findBoxText.className = (isFound) ? "found" : "notfound"; 1495 if (isFound) { 1496 if (this.scrollY == -1) this.scrollY = scrollY; 1497 window.scrollTo(0, 0); 1498 if (SkinPref.getBool("enableFindHighlight", false)) window.setTimeout(this.highlight, 1, Nodes.findBoxText.value); 1499 } 1500 } 1501 }; 1502 1503 /** 1504 * [自動更新] 機能を管理します。 1505 * @static 1506 * @property {Boolean} enabled 自動更新が有効かどうか 1507 */ 1508 var AutoReload = { 1509 /** 自動更新に使うタイマー ID 1510 * @type Number 1511 */ 1512 timerID: 0, 1513 /** 開いているスレッドがアクティヴ状態かどうか 1514 * @type Boolean 1515 */ 1516 isActive: true, 1517 /** 実際に更新を要求するかどうか 1518 * @type Boolean 1519 */ 1520 requestReload: false, 1521 /** このスレッドが閉じられようとしているかどうか 1522 * @type Boolean 1523 */ 1524 isUnloading: false, 1525 /** インターバルの配列 1526 * @type Array 1527 */ 1528 INTERVAL: [15000, 30000, 60000], 1529 /** 各種イベントリスナを登録し、初期化を行います。*/ 1530 initialise: function() { 1531 if (SkinPref.getBool("enableAutoReloadOnLiveThread", false)) 1532 if (EXACT_URL.indexOf("http://live") != -1) this.enabled = true; 1533 if (!SkinPref.getBool("enableAutoReloadWhenInactive", false)) { 1534 window.addEventListener("beforeunload", (function() { this.isUnloading = true }).bind(this), false); 1535 window.addEventListener("focus", (function() { 1536 if (this.enabled && !this.isActive) { 1537 this.isActive = true; 1538 ThreadDocument.setFavicon("", false); 1539 if (this.requestReload) { 1540 if (!this.isUnloading) { 1541 this.timerProc(); 1542 this.enabled = true; 1543 } 1544 this.requestReload = false; 1545 } 1546 } 1547 }).bind(this), false); 1548 window.addEventListener("blur", (function() { 1549 if (this.enabled && this.isActive) { 1550 this.isActive = false; 1551 ThreadDocument.setFavicon("", true); 1552 } 1553 }).bind(this), false); 1554 } 1555 }, 1556 /** タイマーのプロシージャです。*/ 1557 timerProc: function() { 1558 if (this.isActive) { 1559 var items = Nodes.getResItems(); 1560 var lastItem = items[items.length - 1]; 1561 ThreadDocument.reload(!(window.scrollY + window.innerHeight > lastItem.offsetTop + lastItem.offsetHeight)); 1562 this.requestReload = false; 1563 } else { 1564 this.requestReload = true; 1565 } 1566 }, 1567 get enabled() { 1568 return (this.timerID != 0); 1569 }, 1570 set enabled(value) { 1571 if (this.timerID) { 1572 window.clearInterval(this.timerID); 1573 this.timerID = 0; 1574 } 1575 if (value) { 1576 this.timerID = window.setInterval(this.timerProc.bind(this), this.INTERVAL[SkinPref.getInt("valueAutoReloadInterval", 1)]); 1577 } 1578 ThreadDocument.setFavicon(""); 1579 } 1580 }; 1581 1582 /** 1583 * [要約] 機能を管理します。 1584 * @static 1585 * @property {Boolean} enabled 要約が有効かどうか 1586 */ 1587 var Digest = { 1588 _enabled: false, 1589 get enabled() { 1590 return this._enabled; 1591 }, 1592 set enabled(value) { 1593 this._enabled = value; 1594 var expSingleAnchor = new RegExp(/(>?>|>)(\d{1,4})/); 1595 Trackback.traverse(); 1596 if (!Trackback.items) return; 1597 //var bodyItems = ResNodes.getBodyItems(); 1598 var bodyItems = ResNodes.getBodies(); 1599 if (this._enabled) { 1600 for (var i = 0; i < bodyItems.length; i++) { 1601 var body = bodyItems.items(i); 1602 body.parentNode.style.display = ((Trackback.items[body.id.slice(4)]) || (body.textContent.match(expSingleAnchor))) ? "block" : "none" 1603 } 1604 } else { 1605 for (var i = 0; i < bodyItems.length; i++) { 1606 bodyItems.items(i).parentNode.style.display = "block"; 1607 } 1608 } 1609 } 1610 }; 1611 1612 /** 1613 * しおり機能を管理します。 1614 * @static 1615 */ 1616 var Bookmark = { 1617 /** しおりが挿んであるレス番号 1618 * @return Number 1619 */ 1620 index: 0, 1621 /** しおり要素の幅 1622 * @return Number 1623 */ 1624 width: 0, 1625 /** イベントリスナを登録し、しおりが挿んであるレスが非表示であれば表示させます。 1626 * @return {Number} しおりが挿んであるレス番号 1627 */ 1628 initialise: function() { 1629 window.addEventListener("mouseover", this.onMouseOver.bind(this), false); 1630 window.addEventListener("mouseout", this.onMouseOut.bind(this), false); 1631 window.addEventListener("click", this.onClick.bind(this), false); 1632 if (ThreadDocument.boardName && ThreadDocument.threadID) { 1633 this.index = SkinPref.getInt("valueBookmarkIndex@" + ThreadDocument.boardName + "/" + ThreadDocument.threadID, 0); 1634 if (this.index) { 1635 var resStart = Nodes.getRes(this.index); 1636 if (resStart) { 1637 window.scrollTo(0, resStart.offsetTop - (Nodes.header.offsetTop + Nodes.header.offsetHeight)); 1638 this.insert(this.index, true); 1639 } else { 1640 var resItems = Nodes.getResItems(); 1641 var resStart = Nodes.getRes(1); 1642 resStart = (resStart) ? resItems[1] : resItems[0]; 1643 ThreadDocument.asyncInsert(this.index, Nodes.getIndexFromRes(resStart), true, false, 1644 (function(){this.insert(this.index, true)}).bind(this) ); 1645 } 1646 } 1647 } 1648 return this.index; 1649 }, 1650 /** mouseover イベントを処理します。 1651 * @param {event} e イベント 1652 */ 1653 onMouseOver: function(e) { 1654 var node = e.target; 1655 if (node.className == "resContainer") { 1656 if (e.clientX > document.body.clientWidth - 64) { 1657 node.style.cursor = "pointer"; 1658 node.title = (Nodes.getIndexFromRes(node) == this.index) ? "しおりを外す" : "しおりを挿入"; 1659 } else { 1660 if (node.style.cursor == "pointer") { 1661 node.style.cursor = "auto"; 1662 node.title = ""; 1663 } 1664 } 1665 } 1666 }, 1667 /** mouseout イベントを処理します。 1668 * @param {event} e イベント 1669 */ 1670 onMouseOut: function(e) { 1671 var node = e.target; 1672 if (node.className == "resContainer") { 1673 if (node.style.cursor == "pointer") { 1674 node.style.cursor = "auto"; 1675 node.title = ""; 1676 } 1677 } 1678 }, 1679 /** click イベントを処理します。 1680 * @param {event} e イベント 1681 */ 1682 onClick: function(e) { 1683 var node = e.target; 1684 if (node.className == "resContainer") if (e.clientX > document.body.clientWidth - 64) { 1685 var index = Nodes.getIndexFromRes(node); 1686 if (index == this.index) { 1687 this.hide(); 1688 } else { 1689 this.insert(index); 1690 } 1691 } 1692 }, 1693 /** しおりを挿入します。 1694 * @param {Number} index 挿入するレス番号 1695 * @param {Boolean} noAnimatoin アニメーションを無効にするかどうか 1696 */ 1697 insert: function(index, noAnimation) { 1698 var node = Nodes.getRes(index); 1699 if (node) { 1700 var div = document.getElementById("bookmark"); 1701 if (div) div.parentNode.removeChild(div); 1702 var div = document.createElement("div"); 1703 div.id = "bookmark"; 1704 div.title = "しおりを外す"; 1705 div.onmousedown = (function(){ this.hide() }).bind(this); 1706 this.index = index; 1707 SkinPref.setInt("valueBookmarkIndex@" + ThreadDocument.boardName + "/" + ThreadDocument.threadID, this.index); 1708 node.parentNode.insertBefore(div, node); 1709 this.width = div.clientWidth; 1710 if (!noAnimation) this.show(); 1711 } 1712 }, 1713 /** しおりを実際に表示します。 1714 * @param {Number} step しおりの加速度 1715 */ 1716 show: function(step) { 1717 if (!step) step = 1; 1718 var div = document.getElementById("bookmark"); 1719 if (div) { 1720 if (step < this.width) { 1721 div.style.width = step + "px"; 1722 window.setTimeout(this.show.bind(this), 1, step * 4); 1723 } else { 1724 div.style.width = this.width + "px"; 1725 } 1726 } 1727 }, 1728 /** しおりを非表示します。 1729 * @param {Number} step しおりの加速度 1730 */ 1731 hide: function(step) { 1732 if (!step) step = 1; 1733 var div = document.getElementById("bookmark"); 1734 if (div) { 1735 if (step < this.width) { 1736 div.style.width = (this.width - step) + "px"; 1737 window.setTimeout(this.hide.bind(this), 1, step * 4); 1738 } else { 1739 this.index = 0; 1740 SkinPref.remove("valueBookmarkIndex@" + ThreadDocument.boardName + "/" + ThreadDocument.threadID); 1741 div.parentNode.removeChild(div); 1742 } 1743 } 1744 } 1745 }; 1746 1747 1748 /** 1749 * 左右カーソルキーによるスムーズスクロール機能を管理します。 1750 * @static 1751 */ 1752 // スクロール処理中に更にキーが押されたときでも yield でそれっぽく処理できるようになった 1753 var PageScroller = { 1754 /** スクロール位置 1755 * @type Number 1756 */ 1757 y: null, 1758 /** 現在のスクロール対象のレス番号 1759 * @type Number 1760 */ 1761 index: null, 1762 /** 速度 1763 * @type Number 1764 */ 1765 currentVelocity: 0, 1766 /** 初速 1767 * @type Number 1768 */ 1769 initialVelocity: 0, 1770 /** ジェネレータ 1771 * @type Generator 1772 */ 1773 generator: null, 1774 /** まだスクロール中かどうか 1775 * @type Boolean 1776 */ 1777 isBusy: false, 1778 /** ジェネレータの実行をリスタートすべきかどうか 1779 * @type Boolean 1780 */ 1781 shouldRestart: false, 1782 /** イベントリスナを登録します。*/ 1783 initialise: function() { 1784 window.addEventListener("keydown", this.onKeyDown.bind(this), false); 1785 }, 1786 /** keydown イベントを処理します。 1787 * @param {event} e イベント 1788 */ 1789 onKeyDown: function(e) { 1790 var node = e.target; 1791 if (node.tagName == 'TEXTAREA' || node.tagName == 'INPUT' || e.shiftKey || e.ctrlKey || e.altKey) return; 1792 switch (e.which) { 1793 case 37: 1794 e.preventDefault(); 1795 this.prev(); 1796 break; 1797 case 39: 1798 e.preventDefault(); 1799 this.next(); 1800 } 1801 }, 1802 /** 前のレスへスクロールします。*/ 1803 prev: function() { 1804 if (this.isBusy) { 1805 //if (this.index > 0) this.index--; 1806 1807 this.index--; 1808 var resItems = Nodes.getResItems(); 1809 for (var i = this.index ; i >= 0 ; i--) { 1810 if (resItems[i].style.display != "none") break; 1811 this.index--; 1812 } 1813 /* 1814 var resItems = Nodes.getResItems(); 1815 for (var i = this.index - 1; i >= 0; i--) { 1816 if (resItems[i].style.display == "block") { 1817 this.index = i; 1818 break; 1819 } 1820 } 1821 */ 1822 var resItem = resItems[this.index]; 1823 //var resItem = Nodes.getResItems()[this.index]; 1824 if (resItem) { 1825 this.y = resItem.offsetTop - (Nodes.header.offsetTop + Nodes.header.offsetHeight); 1826 this.requestScroll(); 1827 } 1828 } else { 1829 var top = window.pageYOffset + (Nodes.header.offsetTop + Nodes.header.offsetHeight); 1830 var bottom = window.pageYOffset + window.innerHeight; 1831 var resItems = Nodes.getResItems(); 1832 for (var i = resItems.length - 1; i >= 0 ; --i) { 1833 if ((resItems[i].style.display != "none") && (resItems[i].offsetTop < top)) { 1834 var resTop = resItems[i]; 1835 if (resTop) { 1836 this.index = i; 1837 //this.lastDirection = "prev"; 1838 this.y = resTop.offsetTop - (Nodes.header.offsetTop + Nodes.header.offsetHeight); 1839 this.requestScroll(); 1840 } 1841 break; 1842 } 1843 } 1844 } 1845 }, 1846 /** 次のレスへスクロールします。*/ 1847 next: function() { 1848 //if ((this.lastDirection == "next") && (this.isBusy)) { 1849 if (this.isBusy) { 1850 this.index++; 1851 var resItems = Nodes.getResItems(); 1852 for (var i = this.index ; i < resItems.length; i++) { 1853 if (resItems[i].style.display != "none") break; 1854 this.index++; 1855 } 1856 var resItem = resItems[this.index]; 1857 if (resItem) { 1858 //this.y = ((resItem.offsetTop + resItem.offsetHeight) - window.innerHeight); 1859 this.y = resItem.offsetTop - (Nodes.header.offsetTop + Nodes.header.offsetHeight); 1860 this.requestScroll(); 1861 } 1862 1863 } else { 1864 var top = window.pageYOffset + (Nodes.header.offsetTop + Nodes.header.offsetHeight); 1865 var bottom = window.pageYOffset + window.innerHeight; 1866 var resItems = Nodes.getResItems(); 1867 /* 1868 for (var i = 0; i < resItems.length; ++i) { 1869 if (resItems[i].offsetTop > bottom) { 1870 var resTop = resItems[i-1]; 1871 if (resTop) { 1872 this.index = i - 1; 1873 this.lastDirection = "next"; 1874 this.y = ((resTop.offsetTop + resTop.offsetHeight) - window.innerHeight); 1875 this.requestScroll(); 1876 } 1877 break; 1878 } 1879 } 1880 */ 1881 for (var i = 0; i < resItems.length ; i++) { 1882 if ((resItems[i].style.display != "none") && (resItems[i].offsetTop > top)) { 1883 var resTop = resItems[i]; 1884 if (resTop) { 1885 this.index = i; 1886 //this.lastDirection = "next"; 1887 this.y = resTop.offsetTop - (Nodes.header.offsetTop + Nodes.header.offsetHeight); 1888 this.requestScroll(); 1889 } 1890 break; 1891 } 1892 } 1893 if (!resTop) { 1894 this.index = Nodes.getResItems().length - 1; 1895 this.y = document.body.offsetHeight; 1896 this.requestScroll(); 1897 } 1898 1899 } 1900 }, 1901 /** スクロールを要求します。*/ 1902 requestScroll: function() { 1903 if (!(SkinPref.getBool("enableSmoothScroll", true))) return window.scrollTo(0, y); 1904 if (this.isBusy) { 1905 this.shouldRestart = true; 1906 } else { 1907 this.scroll(); 1908 } 1909 }, 1910 /** スクロール処理を実行・管理します。*/ 1911 scroll: function() { 1912 this.generator = this.scrollProc(); 1913 this.isBusy = true; 1914 var y = this.y; 1915 var id = setInterval((function() { 1916 try { 1917 if (this.shouldRestart) { 1918 this.generator.close(); 1919 this.generator = this.scrollProc(); 1920 this.shouldRestart = false; 1921 } 1922 this.generator.next(); 1923 this.generator.next(); 1924 this.generator.next(); 1925 this.generator.next(); 1926 } catch(e) { 1927 clearInterval(id); 1928 this.isBusy = false; 1929 } 1930 }).bind(this) , 0, this.y); 1931 }, 1932 /** 実際のスクロール処理を行います。*/ 1933 scrollProc: function() { 1934 var y1 = window.pageYOffset; 1935 var y2 = parseInt(this.y); 1936 if (y2 < 0) y2 = 0; 1937 if ((y2 + window.innerHeight) > document.body.offsetHeight) y2 = document.body.offsetHeight - window.innerHeight; 1938 var delta = y2 - y1; 1939 switch (SkinPref.getInt("valueSmoothScrollFrames", 1)) { 1940 case 0: 1941 var steps = 16; break; 1942 case 1: 1943 var steps = 24; break; 1944 case 2: 1945 var steps = 32; break; 1946 default: 1947 var steps = 24; 1948 } 1949 //var steps = 32; 1950 this.initialVelocity = this.currentVelocity; 1951 1952 var a1 = (((delta / 2) - this.initialVelocity) * 2) / Math.pow(steps / 2, 2); 1953 var ha1 = a1 / 2; 1954 1955 for (var x = 0; x < steps / 2; ++x) { 1956 yield; 1957 //window.scrollTo(0, (this.initialVelocity * x) + y1 + (ha1 * Math.pow(x, 2))); 1958 window.scrollTo(0, this.initialVelocity + y1 + (ha1 * Math.pow(x, 2))); 1959 this.currentVelocity = (a1 * x); //+ this.initialVelocity; 1960 } 1961 var a2 = (((delta / 2) - 0) * 2) / Math.pow(steps / 2, 2); 1962 var ha2 = a2 / 2; 1963 for (var x = steps / 2 - 1; x >= 0; --x) { 1964 yield; 1965 window.scrollTo(0, y2 - (ha2 * Math.pow(x, 2))); 1966 this.currentVelocity = (a2 * x); 1967 } 1968 window.scrollTo(0, y2); 1969 this.initialVelocity = 0; 1970 this.currentVelocity = 0; 1971 }, 1972 } 1973 1974 /** 1975 * 複数レスの選択を管理します。 1976 * @static 1977 * @property {Number} length 選択されたレスの数 1978 */ 1979 var MultipleResSelector = { 1980 /** マウスボタンが押されているかどうか 1981 * @type Boolean 1982 */ 1983 isButtonDown: false, 1984 /** 選択が開始された要素 1985 * @type element 1986 */ 1987 nodeFrom: null, 1988 /** 選択モード (true: 選択, false: 非選択) 1989 * @type Boolean 1990 */ 1991 modeSelect: false, 1992 get length() { 1993 var items = ResNodes.getSelectors().selected; 1994 return items.length; 1995 }, 1996 /** 選択されたレスのリストを、カンマ区切りの文字列を取得します。 1997 * @return {String} 文字列 1998 */ 1999 getAnchorString: function() { 2000 var items = ResNodes.getSelectors().selected; 2001 if (!items.length) return; 2002 var indexes = []; 2003 var node = items.items(0); 2004 var prevIndex = ResNodes.getIndexByContainer(node.firstChild); 2005 var seqStart = 0; 2006 for (var i = 1; i < items.length; i++) { 2007 var node = items.items(i); 2008 var index = ResNodes.getIndexByContainer(node.firstChild); 2009 if (index == (prevIndex + 1)) { 2010 seqStart = (seqStart > 0) ? seqStart : prevIndex; 2011 } else { 2012 if (seqStart > 0) { 2013 indexes.push(seqStart + "-" + prevIndex); 2014 seqStart = 0; 2015 } else { 2016 indexes.push(prevIndex); 2017 } 2018 } 2019 prevIndex = index; 2020 } 2021 indexes.push((seqStart > 0) ? seqStart + "-" + index : index); 2022 return indexes.join(","); 2023 }, 2024 /** 要素の表示を強制します。 2025 * @param {element} node 要素 2026 */ 2027 ensureVisible: function(node) { 2028 if (node.offsetTop < (window.scrollY + Nodes.header.offsetTop + Nodes.header.offsetHeight)) { 2029 window.scrollTo(0, node.offsetTop - (Nodes.header.offsetTop + Nodes.header.offsetHeight)); 2030 } else if ((node.offsetTop + node.offsetHeight) > (window.scrollY + window.innerHeight)) { 2031 window.scrollTo(0, node.offsetTop - window.innerHeight + node.offsetHeight); 2032 } 2033 }, 2034 /** レスの選択状態を解除します。*/ 2035 clear: function() { 2036 var items = ResNodes.getSelectors().selected; 2037 for (var i = 0; i < items.length; i++) { 2038 var node = items.items(i); 2039 node.className = "resUnselected"; 2040 } 2041 }, 2042 /** イベントリスナを登録します。*/ 2043 initialise: function() { 2044 Nodes.content.addEventListener("mousedown", this.onMouseDown.bind(this), true); 2045 Nodes.content.addEventListener("mouseup", this.onMouseUp.bind(this), true); 2046 Nodes.content.addEventListener("mouseover", this.onMouseOver.bind(this), true); 2047 Nodes.content.addEventListener("contextmenu", this.onContextMenu.bind(this), true); 2048 }, 2049 /** イベントから、マウスの座標が有効範囲内にあるかどうかを取得します。 2050 * @return {Boolean} 有効範囲内であれば true 2051 */ 2052 isEventInRange: function(e) { 2053 return ((e.target.className == "resContainer") && (e.clientX <= 48)) ? true : false; 2054 }, 2055 /** レス要素を選択します。 2056 * @param {element} node 要素 2057 */ 2058 selectNodes: function(nodeTo) { 2059 this.clear(); 2060 //var items = ResNodes.getResItemSelectors(); 2061 var items = ResNodes.getSelectors(); 2062 var firstItem = null; 2063 for (var i = 0; i < items.length; i++) { 2064 var node = items.items(i); 2065 if (!firstItem) firstItem = (node == this.nodeFrom) ? this.nodeFrom : null; 2066 if (!firstItem) firstItem = (node == nodeTo) ? nodeTo : null; 2067 if (firstItem) node.className = "resSelected"; 2068 if (firstItem != node) { 2069 if ((node == this.nodeFrom) || (node == nodeTo)) { 2070 break; 2071 } 2072 } 2073 } 2074 }, 2075 /** mouesdown イベントを処理します。 2076 * @param {event} e イベント 2077 */ 2078 onMouseDown: function(e) { 2079 var node = e.target; 2080 this.isButtonDown = (this.isEventInRange(e) && (e.button == 0)) ? true : false; 2081 if (this.isButtonDown) { 2082 if (!(e.ctrlKey || e.shiftKey)) if (node.parentNode.className != "resSelected") this.clear(); 2083 e.preventDefault(); 2084 this.nodeFrom = node.parentNode; 2085 this.modeSelect = (node.parentNode.className == "resSelected") ? false : true; 2086 this.onMouseOver(e); 2087 } else { 2088 if (e.button == 0) this.clear(); 2089 } 2090 }, 2091 /** mouseup イベントを処理します。 2092 * @param {event} e イベント 2093 */ 2094 onMouseUp: function(e) { 2095 this.isButtonDown = false; 2096 }, 2097 /** mouseover イベントを処理します。 2098 * @param {event} e イベント 2099 */ 2100 onMouseOver: function(e) { 2101 var node = e.target; 2102 if (this.isButtonDown && (node.className == "resContainer")) { 2103 var parentNode = node.parentNode; 2104 //this.selectNodes(parentNode); 2105 parentNode.className = this.modeSelect ? "resSelected" : "resUnselected"; 2106 this.ensureVisible(node); 2107 } 2108 }, 2109 /** コンテキストメニューの項目 2110 * @type Array 2111 */ 2112 items: ([ 2113 ["<b>返信...</b>", "reply"], 2114 ["-"], 2115 [["コピー",SKIN_PATH + "img/copy.png"],[ 2116 ["すべて", "copyAll"], 2117 ["すべて(Jane 形式)", "copyAllJane"], 2118 ["-"], 2119 ["本文", "copyBody"], 2120 ["名前", "copyName"], 2121 ["メール", "copyMail"], 2122 ["日付", "copyDate"], 2123 ["ID", "copyID"], 2124 ["BeID", "copyBeID"], 2125 ["-"], 2126 ["このレスのURL", "copyUrl"] 2127 ]] 2128 ]), 2129 /** ContextMenu オブジェクト 2130 * @type ContextMenu 2131 */ 2132 contextMenu: null, 2133 /** contextmenu イベントを処理します。 2134 * @param {event} e イベント 2135 */ 2136 onContextMenu: function(e) { 2137 var node = e.target; 2138 if (this.isEventInRange(e)) { 2139 if (this.length > 0) { 2140 this.contextMenu = new ContextMenu(this.items); 2141 this.contextMenu.onClick = this.onMenuItemClick.bind(this); 2142 this.contextMenu.show(e.clientX, e.clientY); 2143 e.preventDefault(); 2144 e.stopPropagation(); 2145 } 2146 } 2147 }, 2148 /** メニュー項目がクリックされたときの処理をします。 2149 * @param {String} caption メニュー項目のキャプション 2150 * @param {String} id メニュー項目の ID 2151 */ 2152 onMenuItemClick: function(caption, id) { 2153 switch (id) { 2154 case "reply": 2155 ThreadDocument.writeTo(this.getAnchorString()); 2156 break; 2157 case "copyAll": 2158 case "copyAllJane": 2159 var items = ResNodes.getSelectors().selected; 2160 if (!items.length) return; 2161 var format = (id == "copyAllJane") ? "Jane" : "2ch"; 2162 var buf = []; 2163 for (var i = 0; i < items.length; i++) { 2164 var node = items.items(i); 2165 buf.push(ResNodes.getEntireTextByIndex(ResNodes.getIndexByContainer(node.firstChild), format)); 2166 } 2167 ThreadDocument.setClipboard(buf.join("\n\n")); 2168 break; 2169 case "copyBody": 2170 case "copyName": 2171 case "copyMail": 2172 case "copyDate": 2173 case "copyID": 2174 case "copyBeID": 2175 var f = { 2176 "copyBody": ResNodes.getBodyText.bind(ResNodes), 2177 "copyName": ResNodes.getNameText.bind(ResNodes), 2178 "copyMail": ResNodes.getMailText.bind(ResNodes), 2179 "copyDate": ResNodes.getDateText.bind(ResNodes), 2180 "copyID": ResNodes.getIDText.bind(ResNodes), 2181 "copyBeID": ResNodes.getBeIDText.bind(ResNodes) }; 2182 var items = ResNodes.getSelectors().selected; 2183 if (!items.length) return; 2184 var buf = []; 2185 for (var i = 0; i < items.length; i++) { 2186 var node = items.items(i); 2187 buf.push(f[id](node)); 2188 } 2189 ThreadDocument.setClipboard(buf.join((id == "copyBody") ? "\n\n" : "\n")); 2190 break; 2191 case "copyUrl": 2192 var numbers = this.getAnchorString().split(","); 2193 for (var i = 0; i < numbers.length; i++) { 2194 numbers[i] = EXACT_URL + numbers[i]; 2195 } 2196 ThreadDocument.setClipboard(numbers.join("\n") + "\n"); 2197 break; 2198 } 2199 } 2200 } 2201 2202 /** 2203 * Firefox の検索でヘッダが隠れる問題に対する対処を行います。 2204 * @static 2205 */ 2206 var FxFindHandler = { 2207 /** ドラッグ中かどうか 2208 * @type Boolean 2209 */ 2210 isDragging: false, 2211 /** Shift キーが押されているかどうか 2212 * @type Boolean 2213 */ 2214 isShiftKeyPressed: false, 2215 /** 前回の選択範囲 2216 * @type Range 2217 */ 2218 prevRange: null, 2219 /** 要素の位置とサイズを取得します。 2220 * @param {element} src 要素 2221 * @return {object} left, top, width, height, right, bottom の各メンバから成るオブジェクト 2222 */ 2223 getRect: function(src) { 2224 var parent = src; 2225 var x = 0; 2226 var y = 0; 2227 while (parent) { 2228 x += parent.offsetLeft; 2229 y += parent.offsetTop; 2230 parent = parent.offsetParent; 2231 } 2232 return {left: x, top: y, width: src.offsetWidth, height: src.offsetHeight, 2233 right: x + src.offsetWidth, bottom: y + src.offsetHeight}; 2234 }, 2235 /** 要素の表示を強制します。 2236 * @param {element} node 要素 2237 */ 2238 ensureVisible: function(node) { 2239 var rc = this.getRect(node); 2240 if (rc.top < (window.scrollY + Nodes.header.offsetTop + Nodes.header.offsetHeight)) { 2241 window.scrollTo(0, rc.top - (Nodes.header.offsetTop + Nodes.header.offsetHeight)); 2242 } 2243 }, 2244 /** 選択範囲を保存します。*/ 2245 preventScroll: function() { 2246 var selection = window.getSelection(); 2247 if (!selection.rangeCount) return; 2248 var range = selection.getRangeAt(0); 2249 if (range.collapsed) return; 2250 this.prevRange = range; 2251 }, 2252 /** イベントリスナを登録します。*/ 2253 initialise: function() { 2254 window.addEventListener("mousedown", (function(e) { this.isDragging = (e.button == 0) }).bind(this), false); 2255 window.addEventListener("mouseup", (function(e) { 2256 this.isDragging = false; 2257 this.preventScroll(); 2258 }).bind(this), false); 2259 window.addEventListener("keypress", (function(e) { 2260 this.isShiftKeyPressed = e.shiftKey; 2261 this.preventScroll(); 2262 }).bind(this), false); 2263 window.setInterval(this.onTimer.bind(this), 10); 2264 }, 2265 /** タイマーでマウスやキーイベントを伴わない選択範囲の変化を監視します。 */ 2266 onTimer: function() { 2267 if (this.isDragging || this.isShiftKeyPressed) return; 2268 var selection = window.getSelection(); 2269 if (!selection.rangeCount) return; 2270 var range = selection.getRangeAt(0); 2271 if (range.collapsed) return; 2272 if (range == this.prevRange) return; 2273 // text nodes have no positions 2274 var node = (range.startContainer.nodeType == 3) ? range.startContainer.parentNode : range.startContainer; 2275 this.ensureVisible(node); 2276 this.prevRange = range; 2277 } 2278 } 2279 2280 /** 2281 * 数字キーで該当レスへスクロールする機能を管理します。 2282 * @static 2283 */ 2284 var NumericScroller = { 2285 /** 入力バッファ 2286 * @type String 2287 */ 2288 number: "", 2289 /** 前回のキー入力時の時間 2290 * @type Number 2291 */ 2292 time: 0, 2293 /** 入力バッファをクリアします。*/ 2294 clear: function() { 2295 this.number = ""; 2296 }, 2297 /** keypress イベントを処理します。 2298 * @param {event} e イベント 2299 */ 2300 onKeyPress: function(e) { 2301 if (e.target == Nodes.findBoxText) return; 2302 if ((e.which < 48) || (e.which > 57)) { 2303 this.clear(); 2304 } else { 2305 if (parseInt(this.number) > ThreadDocument.countAll) this.clear(); 2306 if ((new Date().getTime() - this.time) > 1000) this.clear(); // 1000 ms 2307 this.number += String.fromCharCode(e.which); 2308 var node = ResNodes.getContainerByIndex(this.number); 2309 if (node) window.scrollTo(0, node.offsetTop - (Nodes.header.offsetTop + Nodes.header.offsetHeight)); 2310 } 2311 this.time = new Date().getTime(); 2312 }, 2313 /** イベントリスナを追加します。*/ 2314 initialise: function() { 2315 window.addEventListener("keypress", this.onKeyPress.bind(this), false); 2316 } 2317 } 2318 2319 /** 2320 * 不用意なポップアップを禁止する機能を管理します。 2321 * @static 2322 */ 2323 var PopupPreventer = { 2324 /** スクロールされたかどうか 2325 * @type Boolean 2326 */ 2327 isScrolled: false, 2328 /** 最後にスクロールされたときに時間 2329 * @type Number 2330 */ 2331 lastScrolledTime: null, 2332 /** タイマー ID 2333 * @type Number 2334 */ 2335 timerID: null, 2336 /** イベントリスナを登録します。*/ 2337 initialise: function() { 2338 window.addEventListener("mouseover", this.onMouseOver.bind(this), true); 2339 window.addEventListener("scroll", (function() { 2340 this.isScrolled = true; 2341 this.lastScrolledTime = new Date().getTime(); 2342 }).bind(this), true); 2343 window.addEventListener("mousemove", (function() { 2344 if ((new Date().getTime() - this.lastScrolledTime) > 250) this.isScrolled = false; 2345 }).bind(this), true); 2346 }, 2347 /** mouseover イベントを処理します。 2348 * @param {event} e イベント 2349 */ 2350 onMouseOver: function(e) { 2351 if (this.isScrolled) { 2352 e.preventDefault(); 2353 e.stopPropagation(); 2354 } 2355 } 2356 }