jQueryでポップアップメニュー

# 概要

jQueryベースでポップアップメニューを表示し、選択されたメニューに応じたfunction()を実行する方法。

定型なコードだと思うのだけど、実装すると意外にコード量が必要で面倒だった。今後の省力化のためでクラス(っぽいもの)化してみた、って話。左クリック、右クリック(長押し)への紐付けは `click(function(){}), bind("contextmenu", function(){})` で好きなように。

・・・コード量が増えたのは、汎用性を意識したからだ？ハテ？（目を逸らしつつ。

デモは[こちら](http://fluorite.halfmoon.jp/diary/2016/demo_popupmenu/demo_popupmenu.html)。
![popmn.png](https://qiita-image-store.s3.amazonaws.com/0/125588/2631cd71-4f7f-0b9f-65d7-fe43893cac74.png)


なお、jQuery mobile 利用環境であれば、自前実装せずとも`data-role="popup"` を指定することで簡単に実装できるみたい。⇒ http://qiita.com/tyoshikawa1106/items/f1c7824d9f4614d8a71f


# 使い方

`var menu = new PopupMenu( $, {ポップアップメニュー内容});`でインスタンスを生成して、表示したい場所の`click()`等で`menu.show(event, this)`するのみ。

```html:sample.html
<html lang="ja">
<head>
<title>Pupupメニューテスト</title>
<meta http-equiv="Content-Script-Type" content="text/javascript; charset=utf-8" />
<meta name="viewport" id="id_viewport" content="width=device-width" >
<script src="https://code.jquery.com/jquery-1.11.1.min.js"></script>
<script src="./popupmenu.js"></script>
<script language="JavaScript">
<!--
	$(document).ready(function(){
		var menu1 = new PopupMenu( $, [
			{ "title" : "hoge",
			  "action" : function(){ alert("hoge!"); } 
			}, 
			{ "title" : "ふが",
			  "action" : function(){ alert("ふが！"); } 
			}
		]);
		var menu2 = new PopupMenu( $, [
			{ "title" : "piyo",
			  "action" : function(e, targetNode){ 
					var target = $(targetNode);
					alert( "piyo\r\nクリックエリアの内容：\r\n" + target.text() ); 
				} 
			}, 
			{ "title" : "foo",
			  "action" : function(){ alert("foo!"); } 
			}
		]);
		var menu3 = new PopupMenu( $, [
			{ "title" : "右クリックコンテキストメニュー",
			  "action" : function(){ alert("スマホでは長押しに相当"); } 
			}
		]);
		$("#id_div1").click(function(event){
			menu1.show(event, this);
		});
		$("#id_div2").click(function(event){
			menu2.show(event, this);
		});
		$("#id_div3").click(function(){
			menu1.hide(); // これでmenu2, menu3側も閉じる。
		}).bind("contextmenu", function(event){
			menu3.show(event, this);
			return false; // デフォルトのコンテキストメニューを解除
		});
	});
//-->
</script>
</head>
<body bgcolor="#ccddff">
	<div id="id_div1" style="background-color: green;">
		<br>
		<br>
		<br>
		ここでクリックするとポップアップ：その１する<br>
		<br>
		<br>
		<br>
	</div>
	<div id="id_div2" style="background-color: yellow;">
		<br>
		<br>
		<br>
		ここでクリックするとポップアップ：その２する<br>
		<br>
		<br>
		<br>
	</div>
	<div id="id_div3">
		<br>
		<br>
		<br>
		ここで（左）クリックすると閉じる。<br>
		右クリック（か長押し）すると、ポップアップ：その３する。<br>
		<br>
		<br>
		<br>
	</div>
</body>
</html>

```

# ポップアップメニューのクラス定義

上記で利用している、`popupmenu.js` の定義は以下。

```js:popupmenu.js
/**
 * @description ポップアップメニュー表示クラス
 * @param{Object} jQueryCommon 利用するjQuery オブジェクトを渡す。静的に使いまわすので、2回目以降はNULL指定可能。
 * @param{Object} menuList 表示するポップアップメニューリスト。title, action メンバを持つこと。
 */ 
function PopupMenu( jQueryCommon, menuList ){
	/**
	 * @description ポップアップメニューに表示するリスト。title, action メンバを持つ。
	 */
	this.itsMenuList = menuList;
	/**
	 * @description 設定されたポップアップメニューの最大文字列。デフォルト14文字。
	 */
	this.itsMaxStringLength = (function( list, defaultLength ){
		var i, n = list.length, title, action;
		for( i=0; i<n; i++ ){
			title = list[i].title;
			action = list[i].action;
			if( (title && action) && ( defaultLength < title.length ) ){
				defaultLength = title.length;
			}
		}
		return defaultLength;
	}(menuList, 14));

	this.itsId = "_id_popup"; // ※init()の中で数字を付加してユニーク化される。
	this.init( jQueryCommon );
};
(function(){
	/**
	 * @description ポップアップメニューのdiv要素CSS
	 */
	var MENU_DIV_CSS = {
		"position" : "absolute",
		"min-width" : "200px",
		"background-color" : "#eeeeee",
		"border": "1pt solid #666666",
		"box-shadow" : "2px 2px 4px 0px #666666"
	};
	/**
	 * @description ポップアップメニューのulタグのCSS
	 */
	var MENU_UL_CSS = {
		"border": "0pt",
		"margin" : "2px",
		"padding" : "6px",
		"line-height": "12pt",
		"color": "#000",
		"list-style" : "none",
		"font-size" : "11pt"
	};
	/**
	 * @description ポップアップメニュー項目のCSS
	 */
	var LI_CSS = {
		"padding" : "6px",
		"background-color" : "#eeeeee",
		"border" : "1px solid transparent"
	};
	/**
	 * @description ポップアップメニュー項目のHover時のCSS
	 */
	var LI_CSS_HOVER = {
		"background-color" : "#d1e2f2",
		"border" : "1px solid #78aee5"
	}; // http://www.tohoho-web.com/wwwxx035.htm
	/**
	 * @description このクラスで利用するjQuery。
	 */
	var jQ = null;
	/**
	 * @description 生成されたポップアップメニューIDのリスト。
	 */
	var menuIds = [];
	/**
	 * @description 全てのポップアップメニューを非表示にする。
	 * @param{String} exceptId 捜査対象外のID。null指定なら、全部を「非表示」にする。
	 */
	var hideAll = function( exceptId ){
		var n = menuIds.length;
		var target;
		while( 0<n-- ){
			target = jQ("#" + menuIds[n]);
			if( (menuIds[n] != exceptId) && (target.size() > 0) ){
				target.hide();
			}
		}
	}
	/**
	 * @description 最近のshow()で渡された、第一引数Eventと、第二引数。
	 */
	var staticLastEvent = null, staticLastParam = null;
	/**
	 * @description 本クラスの初期化。
	 * @param{Object} initJQuery jQueryオブジェクトを渡す。初回以降はnullでも構わない。
	 */
	PopupMenu.prototype.init = function( initJQuery ){
		var BIND_INDEX_ATTR = "data-action-index";
		var i, list = this.itsMenuList, n = list.length, str, title, action;
		var target, item;
		jQ = (!jQ) ? initJQuery : jQ;
		this.itsId += "_" + menuIds.length;
		target = jQ("body");
		target.append( "<div id='" + this.itsId + "' style='display:none;'><div>" );
		target = jQ("#" + this.itsId);
		target.css( MENU_DIV_CSS );

		str = "<ul>";
		for( i=0; i<n; i++ ){
			title = list[i].title;
			action = list[i].action;
			if( title && action ){
				str += "<li " + BIND_INDEX_ATTR + "='" + i + "'>" + title + "</li>";
			}
		}
		str += "</ul>";
		target.append( str );

		target = target.children("ul").eq(0);
		target.css( MENU_UL_CSS );

		target = jQ("#" + this.itsId + " ul li" );
		target.css( LI_CSS );
		target.hover( 
			function(){ jQ(this).css( LI_CSS_HOVER ); },
			function(){ jQ(this).css( LI_CSS ); } 
		);
		target.click(function(event){
			var item = jQ(this);
			var index = item.attr( BIND_INDEX_ATTR );
			hideAll(null);
			list[ index ].action( staticLastEvent, staticLastParam );
		})

		menuIds.push( this.itsId );
	}
	/**
	 * @description ポップアップメニューを表示する。
	 * 　　　　　　　メニュー以外の場所をクリックすると消える。他のポップアップメニューが表示された時も、以前のは消える。
	 * @param{event} event jQuery.click(function(event){})で渡されるeventオブジェクトを指定する。
	 * 　　　　　　　　　　　呼び出し先の関数の引数にそのまま渡す。
	 * @param{Object} paramObj newの際のactionパラメータに指定された関数呼び出し時の第二引数に渡される。
	 * 　　　　　　　　　　　　　通常はthisを渡すこと。
	 */
	PopupMenu.prototype.show = function(event, paramObj){
		/*
		[メモ] click() はeventが渡される仕様。
		---
		jQueryのイベントは、コールバック関数の最初の引数でjQuery.Eventオブジェクトを受け取ることができます。
		このオブジェクトを使って、規定のイベント動作のキャンセルや、バブリングの抑制などを行います
		http://semooh.jp/jquery/api/events/click/fn/
		*/
		var target = jQ("#" + this.itsId);
		var isShowed = (target.css("display") == "none");
		var pos;

		hideAll( this.itsId );
		if( isShowed ){
			pos = this.getMenuPosition( event );
			target.css({
				"top" : pos.pageY + "px",
				"left" : pos.pageX + "px",
			});
			target.slideDown("fast");
			staticLastEvent = event;
			staticLastParam = paramObj;
		}else{
			target.hide();
		}
	};
	/**
	 * @description ポップアップメニュー表示毎に、表示位置決めのためにcallbackされる。
	 *              サブクラスで任意にオーバーライドのこと。
	 */
	PopupMenu.prototype.getMenuPosition = function( event ){
		return { "pageX" : event.pageX, "pageY" : event.pageY };
	};
	/**
	 * @description 表示中のポップアップメニューを全て非表示にする。
	 */
	PopupMenu.prototype.hide = function(){
		hideAll( null );
	};
}());

```


# 表示位置の任意修正

ポップアップメニューの表示位置を変更する場合は、以下のように上記を継承したサブクラスを生成して行う。下記を用いて `new PopupMenuMobile($,{})` すればOK。デモの2つ目のdivタグにはこちらを適用。 

```js:popupmenu_mobile.js
/**
 * 表示位置を左側へずらしたポップアップメニュー
 */
var PopupMenuMobile = function( jQueryCommon, menuList ){
	PopupMenu.call(this, jQueryCommon, menuList);
};
PopupMenuMobile.prototype = Object.create( PopupMenu.prototype );
PopupMenuMobile.prototype.getMenuPosition = function( event ){
	var nX = event.pageX - this.itsMaxStringLength * 14 + 8;
	nX = (nX > 0) ? nX : 0;
	return { "pageX" : nX, "pageY" : event.pageY };
};

```

以上ー。


# 補足

`hideAll()` のタイミングで、無効なIDは削除した方が良いかもしれない。そこまで大量のメニューを定義するとは思えなかったので、実装しなかったけど。