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","":"2","":"3","":"4","":"5","":"6","":"7","":"8","":"9","":"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 }