1 
  2 /**
  3  * @fileOverview 独自に作成するコンテキストメニューを扱います。
  4  */
  5 
  6 /**
  7  * コンテキストメニューの枠組みを提供します。
  8  * @class
  9  */
 10 function ContextMenu() {
 11     this.initialise.apply(this, arguments);
 12 }
 13 ContextMenu.prototype = {
 14 	/** コンテキストメニューで使用する PopupItem オブジェクト
 15 	 * @type PopupItem
 16 	 */
 17 	popupItem: null,
 18 	/** コンテキストメニューとして表示するメニュー項目
 19 	 * @type Array
 20 	 * @example
 21 	 * [
 22 	 *     ["MenuItem 1", "IDM_MENUITEM1"], // キャプション, ID
 23 	 *     [["MenuItem 2 with icon", "foobar.png"], "IDM_MENUITEM2"], // アイコン付
 24 	 *     ["-"], // 区切り線
 25 	 *     ["SubMenu 1", [ // サブメニュー
 26 	 *         ["SubMenuItem 1", "IDM_SUBMENUITEM1"],
 27 	 *         ["SubMenuItem 2", "IDM_SUBMENUITEM2"]
 28 	 *     ]]
 29 	 * ]
 30 	 */
 31 	items: null,
 32 	/** メニュー項目がクリックされたときに呼ばれる関数
 33 	 * @type Function
 34 	 */
 35 	onClick: null,
 36 	/** コンテキストメニューが削除されるときに呼ばれる関数
 37 	 * @type Function
 38 	 */
 39 	onRemove: null,
 40 	/** サブメニューが親のコンテキストメニューを指し示す ContextMenu オブジェクト
 41 	 * @type ContextMenu
 42 	 */
 43 	parentMenu: null,
 44 	/** 親のコンテキストメニューがサブメニューを指し示す ContextMenu オブジェクト
 45 	 * @type ContextMenu
 46 	 */
 47 	childMenu:  null,
 48 	/** mousedown イベントを処理します。
 49 	 * @param  {event} e イベント
 50 	 */
 51 	onMouseDown: function(e) {
 52 		if (this.popupItem) {
 53 			if ((e.target.rel ? e.target.rel : e.target.parentNode.rel) == "ContextMenu") {
 54 				if (e.button == 1) {
 55 					e.preventDefault();
 56 					e.stopPropagation();
 57 				}
 58 			} else {
 59 				this.remove();
 60 			}
 61 		}
 62 	},
 63 	/** コンストラクタから自動的に呼ばれ、初期化処理を行います。 */
 64 	initialise: function(items) {
 65 		this.items = items;
 66 		window.addEventListener("mousedown", this.onMouseDown.bind(this), false);
 67 	},
 68 	/** コンテキストメニューを表示します。
 69 	 * @param {Number} x1   メニューが表示される原点の X 座標
 70 	 * @param {Number} y1   メニューが表示される原点の Y 座標
 71 	 * @param {Number} [y2] メニューが表示しきれないときに使用される原点の X 座標
 72 	 * @param {Number} [y2] メニューが表示しきれないときに使用される原点の X 座標
 73 	 * @param {ContextMenu} [parentMenu] サブメニューを表示するときに使用される親メニューの ContextMenu オブジェクト
 74 	 */
 75     show: function(x1, y1, x2, y2, parentMenu) {
 76 		this.popupItem = new PopupItem(null);
 77 		this.popupItem.container.className = "contextMenu";
 78 		var ul = document.createElement("ul");
 79 		ul.contextMenu = this;
 80 		ul.popupItemContainer = this.popupItem.container;
 81 		//ul.id = "" + (new Date).getTime();
 82 		ul.rel = "ContextMenu";
 83 		if (parentMenu) {
 84 			this.parentMenu = parentMenu;
 85 			this.parentMenu.childMenu = this;
 86 		}
 87 		for (var i = 0; i < this.items.length; i++) {
 88 			var a = null;
 89 			if (this.items[i][0] == "-") {
 90 				ul.appendChild(document.createElement("hr"));
 91 			} else {				
 92 				var li = document.createElement("li");
 93 				var span = document.createElement("span");
 94 				span.className = "icon";
 95 				if (this.items[i][0] instanceof Array) {
 96 					span.innerHTML = this.items[i][0][0];
 97 					span.style.backgroundImage = "url(" + this.items[i][0][1] + ")";
 98 				} else {
 99 					span.innerHTML = this.items[i][0];
100 				}
101 				span.rel = "ContextMenu";
102 				li.appendChild(span);
103 				li.id = this.items[i][1];
104 				li.rel = "ContextMenu";
105 				if (this.items[i][1] instanceof Array) {
106 					li.className = "more";
107 					li.menuItems = this.items[i][1];
108 					li.onmouseover = function(e) {
109 						e.currentTarget.parentNode.contextMenu.removeChild();
110 						e.currentTarget.className = "selectedMore";
111 						var menu = new ContextMenu(e.currentTarget.menuItems);
112 						menu.onClick = e.currentTarget.parentNode.contextMenu.onClick;
113 						menu.show(
114 							e.currentTarget.parentNode.popupItemContainer.offsetLeft + e.currentTarget.parentNode.popupItemContainer.offsetWidth - 6,
115 							e.currentTarget.parentNode.popupItemContainer.offsetTop + e.currentTarget.offsetTop,
116 							e.currentTarget.parentNode.popupItemContainer.offsetLeft + 6,
117 							e.currentTarget.parentNode.popupItemContainer.offsetTop + e.currentTarget.offsetTop + e.currentTarget.offsetHeight,
118 							e.currentTarget.parentNode.contextMenu
119 						);
120 					};
121 				} else {
122 					li.onmouseover = function(e) {
123 						if (e.currentTarget.parentNode.contextMenu) e.currentTarget.parentNode.contextMenu.removeChild();
124 					};
125 					li.onclick = function(e) { 
126 						var nodePopup = e.currentTarget.parentNode.popupItemContainer;
127 						//nodePopup.parentNode.removeChild(nodePopup);
128 						e.currentTarget.parentNode.contextMenu.remove();
129 						var ret = e.currentTarget.parentNode.contextMenu.onClick(e.currentTarget.textContent, e.currentTarget.id, e);
130 					};
131 					if (li.id.match(/^a:/i)) {
132 						var a = document.createElement("a");
133 						a.rel = "ContextMenu";
134 						a.href = RegExp.rightContext;
135 						a.appendChild(li);
136 					}
137 				}
138 				if (this.items[i].checked) li.className += " checked";
139 				
140 				if (a) {
141 					ul.appendChild(a);
142 				} else {
143 					ul.appendChild(li);
144 				}
145 			}
146 		}
147 		this.popupItem.content.appendChild(ul);
148 		if (!x2) x2 = x1;
149 		if (!y2) y2 = y1;
150 		this.popupItem.container.style.visibility = "hidden";
151 		document.body.appendChild(this.popupItem.container);
152 		if ((window.innerWidth < (x1 + this.popupItem.container.offsetWidth)) && (x2 > this.popupItem.container.offsetWidth)) {
153 			this.popupItem.container.style.left  = x2 - this.popupItem.container.offsetWidth ;
154 		} else {
155 			this.popupItem.container.style.left  = x1;
156 		}
157 		if ((window.innerHeight < (y1 + this.popupItem.container.offsetHeight)) && (y2 > this.popupItem.container.offsetHeight)) {
158 			this.popupItem.container.style.top = y2 - this.popupItem.container.offsetHeight + 12;
159 		} else {
160 			this.popupItem.container.style.top = y1;
161 		}
162 		this.popupItem.container.style.visibility = "visible";
163 		this.popupItem.content.focus();
164     },
165 	/** コンテキストメニューを削除します。*/
166     remove: function() {
167     	var next = this;
168     	while (next.parentMenu) {
169     		if (next.parentMenu.popupItem.container.parentNode) next.parentMenu.popupItem.container.parentNode.removeChild(next.parentMenu.popupItem.container);
170     		var prev = next;
171     		var next = next.parentMenu;
172     		prev.parentMenu = null;
173     	}
174 		next = this;
175     	while (next.childMenu) {
176     		if (next.childMenu.popupItem.container.parentNode) next.childMenu.popupItem.container.parentNode.removeChild(next.childMenu.popupItem.container);
177     		var prev = next;
178     		var next = next.childMenu;
179     		prev.childMenu = null;
180     	}
181     	if (this.popupItem.container.parentNode) this.popupItem.container.parentNode.removeChild(this.popupItem.container);
182     	if (this.onRemove) this.onRemove(this);
183     },
184 	/** コンテキストメニューのサブメニューを削除します。*/
185     removeChild: function() {
186     	var next = this.childMenu;
187     	while (next) {
188     		next.popupItem.container.parentNode.removeChild(next.popupItem.container);
189     		next = next.childMenu;
190     	}
191     	var li = this.popupItem.content.childNodes[0].childNodes;
192     	for (var i = 0; i < li.length; i++) if (li[i].className == "selectedMore") li[i].className = "more";
193     	this.childMenu = null;
194     },
195 };
196 
197 /**
198  * スレッドタイトル部のコンテキストメニューを管理します。
199  * @static
200  */
201 var ThreadNameContextMenu = {
202 	/** コンテキストメニューのメニュー項目
203 	 * @type Array
204 	 */
205 	items: ([
206 		[["コピー",SKIN_PATH + "img/copy.png"],[
207 			["タイトルと URL", "copyTitleAndUrl"],
208 			["-"],
209 			["タイトル", "copyTitle"],
210 			["URL",     "copyUrl"]
211 		]],
212 		["-"],
213 		[["履歴",SKIN_PATH + "img/history.png"],[]],
214 		["-"]
215 		]),
216 	/** ContextMenu オブジェクト
217 	 * @type ContextMenu
218 	 */
219 	contextMenu: null,
220 	/** メニュー項目がクリックされたときの処理をします。
221 	 * @param {String} caption メニュー項目のキャプション
222 	 * @param {String} id      メニュー項目の ID
223 	 * @param {event}  e       オリジナルの click イベントの Event オブジェクト
224 	 */
225 	onClick: function(caption, id, e) {
226 		switch (id) {
227 			case "copyTitleAndUrl":
228 				ThreadDocument.setClipboard(ThreadDocument.title + "\n" + EXACT_URL + "\n");
229 				break;
230 			case "copyTitle":
231 				ThreadDocument.setClipboard(ThreadDocument.title);
232 				break;
233 			case "copyUrl":
234 				ThreadDocument.setClipboard(EXACT_URL);
235 				break;
236 			default:
237 				/*
238 				if (e.button == 0) {
239 					if (id.match(/^thread:/)) location.href = SERVER_URL + RegExp.rightContext + ThreadDocument.option;
240 					if (id.match(/^board:/))  location.href = RegExp.rightContext;
241 				} else if (e.button == 1) {
242 					if (id.match(/^thread:/)) return (SERVER_URL + RegExp.rightContext + ThreadDocument.option);
243 					if (id.match(/^board:/))  return (RegExp.rightContext);
244 				}
245 				*/
246 		}
247 	},
248 	/** contextmenu イベントを処理します。
249 	 * @param {event} e イベント
250 	 */
251 	onContextMenu: function(e) {
252 		if (e.target.id == "threadName") {
253 			this.items = this.items.slice(0,4);
254 			this.items[2][1] = [];
255 			var boards = [];
256 			for (var i = 0; i < HistoryManager.items.length; i++) {
257 				var boardName = HistoryManager.getBoardName(i);
258 				if (!boards[boardName]) {
259 					boards[boardName] = [];
260 					boards[boardName].url = HistoryManager.getBoardUrl(i);
261 				}
262 				boards[boardName].unshift([[HistoryManager.getThreadTitle(i), SKIN_PATH + "img/threaditem.png"], "a:" + SERVER_URL + HistoryManager.getUrl(i) + ThreadDocument.option]);
263 				this.items[2][1].unshift([[HistoryManager.getThreadTitle(i) + " " + "<b>(" + boardName + ")</b>", SKIN_PATH + "img/threaditem.png"], "a:" + SERVER_URL + HistoryManager.getUrl(i) + ThreadDocument.option]);
264 			}
265 			for (var i in boards) {
266 				boards[i].unshift(["-"]);
267 				boards[i].unshift(["<b>スレッド一覧を開く</b>", "a:" + boards[i].url]);
268 				this.items.push([[i, SKIN_PATH + "img/folderclosed.png"], boards[i]]);
269 			}
270 			this.contextMenu = new ContextMenu(this.items);
271 			this.contextMenu.onClick = this.onClick;
272 			this.contextMenu.show(e.target.offsetLeft, e.target.offsetTop + e.target.offsetHeight);
273 			return true;
274 		}
275 	}
276 };
277 
278 /**
279  * スレッドタイトル部のコンテキストメニューを管理します。
280  * @static
281  */
282 var ResNumberContextMenu = {
283 	/** コンテキストメニューのメニュー項目
284 	 * @type Array
285 	 */
286 	items: ([
287 		["<b>返信...</b>", "reply"],
288 		["-"],
289 		[["コピー",SKIN_PATH + "img/copy.png"],[
290 			["すべて", "copyAll"],
291 			["すべて(Jane 形式)", "copyAllJane"],
292 			["-"],
293 			["本文",   "copyBody"],
294 			["名前",   "copyName"],
295 			["メール", "copyMail"],
296 			["日付",   "copyDate"],
297 			["ID",     "copyID"],
298 			["BeID",   "copyBeID"],
299 			["-"],
300 			["このレスのURL",    "copyUrl"]
301 		]]
302 		]),
303 	/** ContextMenu オブジェクト
304 	 * @type ContextMenu
305 	 */
306 	contextMenu: null,
307 	/** トリガー元の要素
308 	 * @type element
309 	 */
310 	target: null,
311 	/** メニュー項目がクリックされたときの処理をします。
312 	 * @param {String} caption メニュー項目のキャプション
313 	 * @param {String} id      メニュー項目の ID
314 	 */
315 	onClick: function(caption, id) {
316 		switch (id) {
317 			case "reply":
318 				if (ResNumberContextMenu.target.href) location.href = ResNumberContextMenu.target.href;
319 				break;
320 			case "copyAll":
321 			case "copyAllJane":
322 				var container = ResNodes.getParentContainer(this.target, true);
323 				var number = ResNodes.getNumbers(container, true).first;
324 				var format = (id == "copyAllJane") ? "Jane" : "2ch";
325 				//ThreadDocument.setClipboard(ResNodes.getEntireTextByIndex(ResNodes.getIndexByContainer(container), format));
326 				ThreadDocument.setClipboard(ResNodes.getEntireTextByIndex(parseInt(number.textContent), format));
327 				break;
328 			case "copyBody":
329 			case "copyName":
330 			case "copyMail":
331 			case "copyID":
332 			case "copyBeID":
333 				var f = {
334 					"copyBody": ResNodes.getBodyText.bind(ResNodes, true),
335 					"copyName": ResNodes.getNameText.bind(ResNodes, true),
336 					"copyMail": ResNodes.getMailText.bind(ResNodes, true),
337 					"copyID":   ResNodes.getIDText.bind(ResNodes, true),
338 					"copyBeID": ResNodes.getBeIDText.bind(ResNodes, true) };
339 				//var container = ResNodes.getParentContainer(this.target);
340 				var container = ResNodes.getParentContainer(this.target, true);
341 				container = container.parentNode;
342 				ThreadDocument.setClipboard(f[id](container));
343 				break;
344 			case "copyUrl":
345 				//var container = ResNodes.getParentContainer(this.target);
346 				var container = ResNodes.getParentContainer(this.target, true);
347 				var number = ResNodes.getNumbers(container, true).first;
348 				ThreadDocument.setClipboard(EXACT_URL + parseInt(number.textContent) + "\n");
349 				//ThreadDocument.setClipboard(EXACT_URL + ResNodes.getIndexByContainer(container) + "\n");
350 				break;
351 		}
352 	},
353 	/** 要素の位置とサイズを取得します。
354 	 * @param  {element} src 要素
355 	 * @return {object} left, top, width, height, right, bottom の各メンバから成るオブジェクト
356 	 */
357 	getRect: function(src) {
358 		var parent = src;
359 		var x = 0;
360 		var y = 0;
361 		while (parent) {
362 			x += parent.offsetLeft;
363 			y += parent.offsetTop;
364 			parent = parent.offsetParent;
365 		}
366 		return {left: x, top: y, width: src.offsetWidth, height: src.offsetHeight,
367 		        right: x + src.offsetWidth, bottom: y + src.offsetHeight};
368 	},
369 	/** contextmenu イベントを処理します。
370 	 * @param {event} e イベント
371 	 */
372 	onContextMenu: function(e) {
373 		var node = e.target.parentNode;
374 		if (node) if (node.className == "resNumber") {
375 			this.target = e.target;
376 			this.contextMenu = new ContextMenu(this.items);
377 			this.contextMenu.onClick = this.onClick.bind(this);
378 			var rc = this.getRect(this.target);
379 			this.contextMenu.show(rc.left - window.scrollX, rc.top + rc.height - window.scrollY);
380 			return true;
381 		}
382 	}
383 }
384 
385 var ChevronContextMenu = {
386 	/** コンテキストメニューのメニュー項目
387 	 * @type Array
388 	 */
389 	items: ([
390 		["自動更新", "enableAutoReload"],
391 		["-"],
392 		["要約", "digest"],
393 		["-"],
394 		["Moreyon(仮)", "enableMoreyon"],
395 		["スレッド情報...", "showInfo"],
396 		[["コピー",SKIN_PATH + "img/copy.png"],[
397 			["タイトルと URL", "copyTitleAndUrl"],
398 			["-"],
399 			["タイトル", "copyTitle"],
400 			["URL",     "copyUrl"]
401 		]],
402 		["-"],
403 		["オプション...", "showOption"]
404 		//["-"],
405 		/*[["履歴",SKIN_PATH + "img/history.png"],[]],*/
406 		]),
407 	/** ContextMenu オブジェクト
408 	 * @type ContextMenu
409 	 */
410 	contextMenu: null,
411 	/** メニュー項目がクリックされたときの処理をします。
412 	 * @param {String} caption メニュー項目のキャプション
413 	 * @param {String} id      メニュー項目の ID
414 	 */
415 	onClick: function(caption, id) {
416 		switch (id) {
417 			case "enableAutoReload":
418 				this.items[0].checked = this.items[0].checked ? false : true;
419 				AutoReload.enabled = this.items[0].checked;
420 				break;
421 			case "digest":
422 				this.items[2].checked = this.items[2].checked ? false : true;
423 				Digest.enabled = this.items[2].checked;
424 				break;
425 			case "enableMoreyon":
426 				this.items[4].checked = this.items[4].checked ? false : true;
427 				Moreyon.enabled = this.items[4].checked;
428 				break;
429 			case "showInfo":
430 				Analyse.analyse();
431 				break;
432 			case "copyTitleAndUrl":
433 				ThreadDocument.setClipboard(ThreadDocument.title + "\n" + EXACT_URL + "\n");
434 				break;
435 			case "copyTitle":
436 				ThreadDocument.setClipboard(ThreadDocument.title);
437 				break;
438 			case "copyUrl":
439 				ThreadDocument.setClipboard(EXACT_URL);
440 				break;
441 			case "showOption":
442 				dialogOptions.show();
443 				break;
444 		}
445 		this.contextMenu = null;
446 	},
447 	/** コンテキストメニューが削除されたときの処理をします。*/
448 	onRemove: function() {
449 		this.contextMenu = null;
450 	},
451 	/** mousedown イベントを処理します。
452 	 * @param {event} e イベント
453 	 */
454 	onMouseDown: function(e) {
455 		var node = e.target;
456 		if (node) if (node.id == "chevron") {
457 			if (this.contextMenu) {
458 				this.contextMenu.remove();
459 				this.contextMenu = null;
460 				return;
461 			}
462 			this.items[0].checked = AutoReload.enabled;
463 			this.items[2].checked = Digest.enabled;
464 			this.items[4].checked = Moreyon.enabled;
465 			this.contextMenu = new ContextMenu(this.items);
466 			this.contextMenu.onClick = this.onClick.bind(this);
467 			this.contextMenu.onRemove = this.onRemove.bind(this);
468 			e.cancelBubble = true;
469 			e.preventDefault();
470 			this.contextMenu.show(document.body.clientWidth, 32);
471 		}
472 	}
473 }
474 
475 document.addEventListener("contextmenu", function(e) {
476 	if (ThreadNameContextMenu.onContextMenu(e) ||
477 		ResNumberContextMenu.onContextMenu(e)) {
478 		e.cancelBubble = true;
479 		e.preventDefault();
480 	}
481 }, true);
482 window.addEventListener("mousedown", ChevronContextMenu.onMouseDown.bind(ChevronContextMenu), true);
483