[JS, jQuery]ページ内に他のページをiframeのように埋め込む

一昔前まではサイトそのものをフレーム構造にしたり iframe を使って新着情報などを埋め込むサイトを良く見かけましたが、HTML5 と Ajax が定着してからはあまり見なくなりました。frameset や frame タグ自体廃止され、唯一残った iframe も iPhone や android の登場により使いにくいものとなってしまったことが理由の一つかと思います。
今回は jQuery の load() を使って他のページを GET, POST の二つの方式を用いて読み込むサンプルを作ってみようと思います。


GET

GET

index.html

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title>Ajax Load</title>
		<script src="//code.jquery.com/jquery-1.12.0.min.js"></script>
		<script>
		function loadPage(page){
			$("#content-frame").load("pages.php?page=" + page);
		}
		</script>
	</head>
	<body>
		<ul>
			<li><a href="javascript:loadPage(0);">Page 1</a></li>
			<li><a href="javascript:loadPage(1);">Page 2</a></li>
			<li><a href="javascript:loadPage(2);">Page 3</a></li>
		</ul>
		<div id="content-frame" style="width: 300px;height:200px;overflow:scroll;border:1px solid #DDD;">
			Sample
		</div>
	</body>
</html>

pages.php

<?php
$content1 = <<<EOD
<h1>Hello, World!</h1>
<p>page 1</p>
EOD;

$content2 = <<<EOD
<h1>Foo Bar</h1>
<p>page 2</p>
EOD;

$content3 = <<<EOD
<h1>Lorem Ipsum</h1>
<p>page 3</p>

<p>
Lorem ipsum dolor sit amet,
consectetur adipiscing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris
nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor
in reprehenderit in voluptate velit esse cillum dolore eu fugiat
nulla pariatur. Excepteur sint occaecat cupidatat non proident,
sunt in culpa qui officia deserunt mollit anim id est laborum.
</p>
EOD;

$pages = array($content1, $content2, $content3);

$page = filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT);
$page = ($page !== false) ? (int)$page : 0;

if(isset($pages[$page])){
	echo $pages[$page];
} else {
	echo 'Not Found.';
}

GET パラメータ「page」を pages.php を渡し、そのページ番号に応じて HTML を返すという単純なものです。
$("#表示先ID").load(読み込むページのURL); という形で指定します。 iframe と違い要素内に直接読み込まれるので <html> や<body> などのタグは使いません。
また、CSS などは読み込み元のページのスタイルが適用されます。


POST

POST

index.html

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title>Ajax Load</title>
		<script src="//code.jquery.com/jquery-1.12.0.min.js"></script>
		<script>
		function submitMessage(page){
			var params = {message: $('#message').val()};
			$("#content-frame").load("hello.php", params);
		}
		</script>
	</head>
	<body>
		<p>
			<input id="message" type="text">
			<input type="button" onclick="submitMessage()" value="Submit">
		</p>

		<div id="content-frame" style="width: 300px;height:200px;overflow:scroll;border:1px solid #DDD;">
			Sample
		</div>
	</body>
</html>

hello.php

<?php
$message = (string)filter_input(INPUT_POST, 'message');
echo '<p>Hello, ' . htmlspecialchars($message, ENT_QUOTES, 'UTF-8') . '!</p>';

load() の第二引数にオブジェクトを指定した場合 POST メソッドが使用されます。
サンプルではテキストボックスの文字列を post.php に送信し、post.php は送られてきた文字に「Hello, 」をつけて返します。

POST の際にCSRF(クロスサイトリクエストフォージェリ)対策としてトークンを送信する必要がある場合、ajaxSetup() を使って拡張ヘッダに埋め込んでしまうのが良いと思います。

<meta name="csrf-token" content="<?php echo $token;?>">

<script>
$.ajaxSetup({
    headers: {
        'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
    }
});
</script>

送信されたヘッダのトークンは PHP では $_SERVER["HTTP_X_CSRF_TOKEN"]; として受け取ることができます。

また、より複雑な通信を行う必要がある場合は load() の代わりに ajax() を使うことになります。
以下は同じ動作を ajax() を使う形に変更したものです。

function submitMessage(page){
    var params = {message: $('#message').val()};
    $("#content-frame").load('hello.php', params);

    $.ajax({
        url: "hello.php",
        data: params,
        type: "POST",
        success: function(html) { $("#content-frame").html(html); }
    });
}

[JS]TinyMCEでレスポンシブな独自のダイアログを使う(Bootstrap)

TinyMCE(4.x)ではリンク挿入ボタンや画像選択ボタンを押すとダイアログがポップアップ表示されますが、レスポンシブに非対応なため、iPhone や Android などのスマートフォンでは見切れてしまい全体を表示できません。

そこで、TinyMCE に用意されたポップアップウィンドウを利用せず、Bootstrap(v3) などで作った独自のモーダルダイアログを表示するプラグインを作ってみます。

プラグイン名は「myplugin」で、ボタンを押すとダイアログが表示され、決定ボタンで「Hello, World!」という文字が挿入されます。

DEMO

スクリーンショット 2015-09-04 17.59.39

まずは tinymce/plugins/myplugin/plugin.min.js となるようにプラグインフォルダと JS ファイルを作成します。
plugin.min.js の内容は下記のとおりです。

tinymce.PluginManager.add('myplugin', function( editor ) {
	editor.addCommand('MyCommand', function() {
		window.myObj.open(editor);
	});

	editor.addButton('myplugin', {
		text: 'My Plugin',
		icon: false,
		tooltip: 'This is my plugin.',
		cmd: 'MyCommand',
	});

	editor.addMenuItem('myplugin', {
		text: 'My Plugin',
		cmd: 'MyCommand',
		context: 'insert',
		prependToContext: true
	});
});

editor.addCommand() でボタンやメニューを押した時の動作を登録します。コマンド名は「MyCommand」としましたが自由に一意な名前をつけて下さい。editor.addButton() でボタンが追加され、editor.addMenuItem() でメニューが追加されます。cmd には先ほどのコマンド名を指定します。

addCommand() を使わずにメニューやボタンに直接 onclick を持たせる書き方もあります。この場合コマンドは登録されないので execCommand() では呼び出せなくなります。

tinymce.PluginManager.add('myplugin', function( editor ) {

	editor.addButton('myplugin', {
		text: 'My Plugin',
		icon: false,
		tooltip: 'This is my plugin.',
		onclick: function(){ window.myObj.open(editor); }
	});

	editor.addMenuItem('myplugin', {
		text: 'My Plugin',
		onclick: function(){ window.myObj.open(editor); },
		context: 'insert',
		prependToContext: true
	});
});

TinyMCE を表示するフォームは次のようになっています。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>title</title>
<script src="https://code.jquery.com/jquery-1.11.3.min.js"></script>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
<script src="js/tinymce/tinymce.min.js"></script>
<script>
var myObj;

$(function(){
    var editor;
    myObj = {
        init: function(){
            $('#mySubmit').on('click', myObj.insert);
        },
        open: function(ed){
            editor = ed;
            $('#myModal').modal('show');
        },
        insert: function(){
            editor.execCommand('mceInsertContent', false, "Hello, World!");
            $('#myModal').modal('hide');
        }
    };
    $(document).ready(function(){
        myObj.init();
    });
});
</script>

<script>
tinymce.init({
    selector: '#wysiwyg',
    plugins: ["myplugin"],
    toolbar: "myplugin"
});
</script>
</head>
<body>
<div class="container">
<h1>TinyMCE Demo</h1>

<textarea id="wysiwyg"></textarea>
</div>

<!-- Modal -->
<div class="modal fade" id="myModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
  <div class="modal-dialog" role="document">
    <div class="modal-content">
      <div class="modal-header">
        <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
        <h4 class="modal-title" id="myModalLabel">Modal title</h4>
      </div>
      <div class="modal-body">
        <p>Hello, World!</p>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
        <button type="button" class="btn btn-primary" id="mySubmit">Submit</button>
      </div>
    </div>
  </div>
</div>
</body>
</html>

下記は TinyMCE の初期設定部分です。
selector にエディタを表示する textarea の ID を指定し、plugins に今回使用するプラグイン名「myplugin」を指定します。ツールバーにも表示する場合は toolbar にも指定します。

tinymce.init({
    selector: '#wysiwyg',
    plugins: [ "myplugin"],
    toolbar: "myplugin"
});
</script>

myObj.open() ではダイアログの表示処理を、myObj.insert() ではテキストエリアに文字を挿入する処理が行われます。open() 時にプラグインから渡された editor を受け取って保管しておき、insert() でそれを使います。editor.execCommand() は TinyMCE の各種コマンドを実行できます。今回はテキストの挿入なので mceInsertContent を使いました。その他のコマンドはこちらに掲載されています。(ver.3.x用)

[JS]HTML5 Canvasを使ったライフゲーム

HTML5 で導入された要素 Canvas には自由に図形を描画でき、JavaScript と組み合わせることでリアルタイムに書き換えることができます。
そこで練習も兼ねて HTML5 + JavaScript でシンプルなライフゲームを作ってみました。

lifegame

Demo

・html 部分

<!DOCTYPE html>
<html>
<head>
<title>Game of Life</title>
<meta charset="utf-8">
<script src="lifegame.js"></script>
</head>
<body>

<div style="margin: 1em 0;">
<form>
<input type="button" value="Random" id="buttonRandom">
<input type="button" value="Start" id="buttonStart">
<input type="button" value="Reset" id="buttonReset">
</form>
</div>

<canvas id="lifegame" width="320" height="320"></canvas>
</body>
</html>

・スクリプト部分(lifegame.js)

var canvas;
var ctx;
var cellSize = 8;	// セル1マスのサイズ
var cols;
var rows;
var cells = new Array();
var buttonStart;
var buttonRandom;
var buttonReset;
var timer1;
var running = false;

window.onload = function()
{
	canvas = document.getElementById('lifegame');
	ctx = canvas.getContext('2d');
	cols = Math.floor(canvas.width / cellSize);
	rows = Math.floor(canvas.height / cellSize);
	initCells();
	buttonStart = document.getElementById('buttonStart');
	buttonRandom = document.getElementById('buttonRandom');
	buttonReset = document.getElementById('buttonReset');
	buttonStart.addEventListener('click', onStart, false);
	buttonRandom.addEventListener('click', randomCells, false);
	buttonReset.addEventListener('click', initCells, false);
	canvas.addEventListener('click', canvasClick, false);
};

// 開始
function onStart(){
	if(running){
		clearInterval(timer1);
		buttonStart.value = "Start";
		running = false;
	} else {
		nextGeneration();
		timer1 = setInterval("nextGeneration()", 100);
		buttonStart.value = "Stop";
		running = true;
	}
}

// 初期化
function initCells(){
	ctx.fillStyle = 'rgb(60, 60, 60)';
	ctx.fillRect(0,0, canvas.width, canvas.height);
	for(col=0;col<cols;col++){
		cells[col] = new Array();
		for(row=0;row<rows;row++){
			cells[col][row] = 0;
		}
	}
	redraw();
}

// ランダムに埋める
function randomCells(){
	for(col=0;col<cols;col++){
		cells[col] = new Array();
		for(row=0;row<rows;row++){
			cells[col][row] = Math.round( Math.random());
		}
	}
	redraw();
}

// 全体を再描画
function redraw(){
	for(col=0;col<cols;col++){
		for(row=0;row<rows;row++){
			drawCell(col, row);
		}
	}
}

// セルを描画
function drawCell(x, y){
	var value = cells[x][y];
	var style = value ? "rgb(156, 255,0)" : "rgb(40,40,40)"; 
    ctx.fillStyle = style;
	ctx.fillRect(x * cellSize, y * cellSize,
		cellSize - 1, cellSize - 1);
}

// 世代を進行させる
function nextGeneration(){
	var tmpCells = new Array();
	for(col=0;col<cols;col++){
		tmpCells[col] = new Array();
		for(row=0;row<rows;row++){
			var count = countAround(col, row);
			if(cells[col][row]){
				if(count == 2 || count == 3){
					tmpCells[col][row] = 1;
				} else {
					tmpCells[col][row] = 0;
				}
			} else {
				if(count == 3){
					tmpCells[col][row] = 1;
				} else {
					tmpCells[col][row] = 0;
				}
			}
		}
	}
	cells = tmpCells;
	redraw();
}

// 周囲の生存セルを数える
function countAround(x, y){
	var count = 0;
	for(i=-1;i<=1;i++){
		for(j=-1;j<=1;j++){
			if(
				(i != 0 || j != 0) &&
				x + i >= 0 && x + i < cols &&
				y + j >= 0 && y + j < rows
			) {
				count += cells[x + i][y + j];
			}
		}
	}
	return count;
}

// Canvasクリック
function canvasClick(e){
    var x = e.clientX - canvas.offsetLeft;
    var y = e.clientY - canvas.offsetTop;
	var col = Math.floor(x / cellSize);
	var row = Math.floor(y / cellSize);
	cells[col][row] = !cells[col][row];
	drawCell(col, row);
}

ランダムボタンでランダムにセルを埋めるかマウスクリックでセルの ON/OFF を設定してスタートボタンで開始します。
セルの周囲に2~3の生存セルが隣接している場合生き残り、過疎(1以下)や過密(4以上)の場合消滅します。
死んでいるセルの周囲にちょうど3つの生存セルが隣接している場合生きているセルが発生します。