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