HTMLで縦書き台本を描画

声劇台本を投稿できるサイトを作成しました。

ボイコネというSNSアプリのフォーマットの台本をそのまま投稿できるようにしています。
ボイコネユーザーの脚本家の方は、編集の手間なしに以下Webサイトに台本を投稿し、Webで公開することができます。

ただ、Web上にある台本サイトはたいてい横書きなのですが、横書きだと非常にセリフが読みづらい・・・。
そのため、縦書きで台本を表示できるかを試してみました。

## 環境
　・PHP7.4 （フレームワーク：fuelPHP)
　・MariaDB
　・jQuery3.3
　・Bootstrap4


## やりたいこと
　・DBに格納された台本テキストを横書きHTMLで描画
　・「縦書きで表示」ボタンを押すとモーダルで縦書き画面を表示

## DBに格納された台本テキスト原文
<img src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/200881/050a83e9-eb2e-b93d-c4e8-1c770c687cc0.png" width = "500">

## ブラウザ上での表示（横書き）　
<img src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/200881/ed953e58-2ca1-c140-0f26-e67e583091c4.png" width = "700">

## ブラウザ上での表示（縦書き） 
※ボタンを押すと縦書きに切り替わる 
<img src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/200881/7a3308a8-da4d-f695-0655-383b8bf9e762.png" width = "700">

## 処理内容説明
- DBに格納された台本テキストをPHPで描画
- 上記テキストをjQueryでテーブル形式の横書き台本として描画
- 上記テキストをjQueryで縦書き台本として描画。非表示に。
- 「縦書きに切り替え」ボタンを押したときのみ、モーダルで縦書きテキストを表示

# コード （一部のみ抜粋）
```PHP:detail.php
	<!-- 縦書き台本描画欄（モーダル） -->
	<div id = "vertical_modal_container" class = "d-none">
		<div id = "vertical_modal_body">
			<div id = "vertical_button_area" class = "clearfix">	
				<div class = "float-right">		
					<input type = "button" id = "close_vertical_button" class = "btn btn-outline-secondary" value = "× 縦書き画面を閉じる"></input>		
				</div>
			</div>	
			<div id = "vertical_script_area" class = "">
				<!-- 台本のテキストを描画 -->
				<?php echo $script_body; ?>		
			</div>
		</div>	
	</div>

	<!-- 横書き台本描画欄 -->
	<div id = "horizon">	
		<!-- 縦書きに切り替えボタン -->
		<div id = "show_by_vertical_button_area" class = "clearfix">
			<div class = "float-right">		
				<input type = "button" id = "show_by_vertical_button" class = "btn btn-outline-secondary" value = "縦書きで表示"></input>		
			</div>
		</div>	

		<!-- 台本のテキストを描画（非表示）-->
		<div id = "script_write_area" class = "d-none">
			<?php echo $script_body; ?>
		</div>
		<!-- セリフ描画欄（テーブル）-->
		<div id = "script_show_area">
		</div>		
	</div>
```

```CSS:style.css
/*======================================================
/* 台本描画欄（縦書き）
//=====================================================*/

/* モーダル背景 */
#vertical_modal_container {
	position: fixed;
	top: 0;
	left: 0;
	width: 100%;
	height: 100%;
	padding: 36px 8px;
	overflow: auto;
	background: rgba(0,0,0,60%);
	opacity: 0;
	transition: .3s;	
	visibility: hidden;
	box-sizing: border-box;
  }
/* モーダル背景 アクティブ時 */
#vertical_modal_container.active{
	opacity: 1;
	visibility: visible;
}  
/* モーダル本体 */
div#vertical_modal_body {
	width: 100%;
	max-width: 1400px;
	margin: 0 auto;
	height: 95%;
}
/* 閉じるボタンの枠 */
div#vertical_button_area {
	width: 100%;
	padding: 8px 8px;
	text-align: right;
	font-size: 100%;
	background-color: #EDE3CB;	
	border: solid 1px #666666;
}
/* 閉じるボタン */
div#vertical_button_area input {
	font-size: 90%;
	background-color: #ffffff;
}
div#vertical_button_area input:hover {
	font-size: 90%;
	background-color: #666666;
}
/* 台本描画欄 */
div#vertical_script_area  {
	width: 100%;	
	height: 100% auto;	
	-ms-writing-mode: tb-rl;
	writing-mode: vertical-rl;
	overflow: scroll;
	padding: 16px 36px;
	background-color: #ffffff;
	border-right: solid 1px #333333;	
	border-left: solid 1px #333333;		
	border-bottom: solid 1px #333333;	
}
/* 台本描画欄 キャラ名 + セリフ */
div#vertical_script_area div {
	padding: 4px;
	background-color: #ffffff;
	cursor: pointer;
}
/* キャラ名 */
div#vertical_script_area span {
	display: block;
	min-height: 80px;
	height: fit-content;
	padding: 8px 0px;
	border-radius: 0px;
}
/* キャラ名の色分け */
span.ffffff {
	color: #ffffff;
	background: #ffffff; 
}
span.fbf8c4 {
	background: #fbf8c4; 
	border: solid 1px #bcbb84;
}
span.ffe0e0 {
  background: #ffe0e0;
  border: solid 1px #d6b8b8;	
}
span.e2f2fe {
  background: #e2f2fe;
  border: solid 1px #a2bbcd;	
}
span.edf9d7 {
  background: #edf9d7;
  border: solid 1px #a2bd6a;	
}
span.f7eed0 {
  background: #f7eed0;
  border: solid 1px #c2aa5c;	
}
/* セリフ */
div#vertical_script_area p {
	margin: 12px 12px;
	font-size: 120%;	
	cursor: pointer;
}

/* 縦書き第本描画欄 モバイル */
@media only screen and (max-width: 750px) {
	div#vertical_button_area {
		font-size: 80%;
	}	
	div#vertical h2 {
		font-size: 120%;
		margin-left: 30px;
	}
	div#vertical_script_area span {
		font-size: 90%;
		padding: 6px 4px 6px 2px;
	}
	div#vertical_script_area p {
		margin: 8px 8px;
		font-size: 105%; /*110%*/
	}	
}

/* 縦書き台本描画欄 モバイル横向き */
@media (orientation: landscape) {
	/* モーダル背景 */
	#vertical_modal_container {
		padding: 0px 10px;
	}
	/* ボタンエリア */
	div#vertical_button_area {
		width: 100%;
		padding: 4px 4px;
		text-align: right;
		font-size: 100%;
		background-color: #EDE3CB;	
		border: solid 1px #666666;
	}
	div#vertical_button_area input {
		font-size: 80%;
		background-color: #ffffff;
	}
	div#vertical_script_area p {
		margin: 8px 8px;
		font-size: 100%;
	}	
}
```

```Javascript:common.js
    /*======================================================
	/*
	/* 3-1. 台本詳細ページ イベント 縦書きに切り替えボタンクリック
	/*
	/*======================================================*/	
	
	/**
	 * 台本詳細ページ イベント 縦書きに切り替えボタンクリック
	 */
	$("#show_by_vertical_button").click(function() {
		$("#vertical_modal_container").removeClass("d-none")		
		$("#vertical_modal_container").show();		
		$("#vertical_modal_container").addClass('active');	
		$("#horizon").hide();		
	});

	/*======================================================
	/*
	/* 3-2. 台本詳細ページ イベント 縦書き画面を閉じるボタンクリック
	/*
	/*======================================================*/	
	
	/**
	 * 台本詳細ページ イベント 縦書きに切り替えボタンクリック
	 */
	$("#close_vertical_button").click(function() {
		$("#vertical_modal_container").removeClass('active');
		$("#horizon").show();		
	});

	/*======================================================
	/*
	/* 3-3. 台本詳細ページ セリフをテーブル形式で描画（横書き）
	/*
	/*======================================================*/		

	/**
	 * セリフをテーブル形式で描画（横書き）
	 */
	 function show_txt_by_table() {
		var NARRE_TXT_PREFIX = "――― "
		var table_tag = "<table class = 'script'><tr><th></th><th>　</th></tr>"

		// 台本のテキストを改行で分割して取得	
		var line_txts = get_line_txts_from_div($("#script_write_area"));
		// テキストから名前だけをセット
		var names = get_names(line_txts)
		
		// 行を走査
		for ( i = 0; i < line_txts.length; i++ ) {
			// 該当行のテキストをセット
			var ltxt = line_txts[i];
			// 名前をセット 
			var txts = ltxt.split("：");
			var name = txts[0]
			// セリフをセット
			var vc = txts[1]

			// 前の行の氏名をセット
			var p_name = get_line_name(line_txts, i - 1)

			// 前の行の氏名と異なれば新しい行へ
			if ( p_name != name ) {
				// 1行目以外は閉じタグを追加
				if ( i != 0 ) {
					table_tag += "</td></tr>"
				}

				var ncl = get_name_class(names, name);
				table_tag += "<tr class = '" + ncl + "'><td class = 'name'><span class = '" + ncl + "'>" + name + "</td>" + "<td class = 'text'>" 

				// ト書きの場合のみ、--を追加
				if (name == "0") {
					vc = NARRE_TXT_PREFIX + vc
				}

				// セリフを描画
				table_tag += vc; 							
			// 前の行と氏名が同じなら改行＋セリフのみ
			} else {
				table_tag += "<br />" + vc
			}
		}

		table_tag += "</tr></table>"

		// 描画
		$("#script_show_area").html(table_tag);
	 }

	//======================================================
	// divタグから "：" 付の行のテキストのみを配列で返す
	//======================================================

	 /**
	  * divタグから：付の行のテキストのみを配列で返す
	  */ 
	 function get_line_txts_from_div(div_block) {
		var line_txts = []

		// 行で分割
		var splits = div_block.text().split(/\r\n|\n/);			
		for (i = 0; i < splits.length; i++) {
			var line_txt = splits[i].trim();

			// :を含めば
			if (line_txt.indexOf("：") != - 1) {
				line_txts.push(line_txt)
			}
		}

		return line_txts;
	 }

	//======================================================
	// 台本の該当行のキャラ名を返す
	//======================================================

	 /**
	  * 台本の該当行のキャラ名を返す
	  * 　・例："キャラ1：セリフ" → "キャラ1"
	  * 
	  * @param line_txts 第本テキスト
	  * @param i 行番号
	  * @return キャラ名
	  */ 
	 function get_line_name(line_txts, i) {
		// 0行目未満ならスキップ
		if ( i < 0 ) {
			return ""
		}

		// 該当行のテキストをセット
		var ltxt = line_txts[i]
		// "："で分割
		var txts = ltxt.split("：");

		return txts[0]
	 }

	//======================================================
	// キャラ名だけを配列で返す
	//======================================================	 

	 /**
	  * キャラ名だけを配列で返す
	  */
	function get_names(line_txts) {
		var names = []

		// 台本テキストを走査
		for ( i = 0; i < line_txts.length; i++ ) {
			// 該当行のテキストをセット
			var ltxt = line_txts[i];

			// "："で分割して名前をセット 
			var txts = ltxt.split("：");
			var name = txts[0]

			if ( names.indexOf(name) == -1 ) {
				names.push(name)
			}
		}

		return names
	}

	//======================================================
	// 該当のキャラ名のspanタグ用クラスを返す
	//======================================================	 	

	/**
	 * 該当のキャラ名のspanタグ用クラスを返す
	 * 　・"0"の場合は"ffffff"を返す
	 * 　・キャラクター紹介用テーブルから該当キャラのクラスを返す
	 * 
	 * @return cl_txt 
	 */ 
	function get_name_class(names, name) {
		var txt = "";
		const NARRATION_CLASS = "ffffff"

		// 0の場合は"ffffff"を返す
		if (name == "0") {
			return NARRATION_CLASS;
		}

		// キャラテーブルを走査
		$('table#chara tr').each(function(i) {
			// nameセルをセット
			var name_td = $(this).find("td.name");
			var span_td = $(this).find("span");

			// 名前が一致すれば
			if (span_td.text() == name) {
				var cl = span_td.attr("class");
				cl_txt = cl;

				return;
			} else if(name.indexOf(span_td.text()) !== -1) {
				var cl = span_td.attr("class");
				cl_txt = cl;				
			}
		});		

		return cl_txt;
	}
	
	/*======================================================
	/*
	/* 3-4. 台本詳細ページ セリフを描画（縦書き）
	/*
	/*======================================================*/		

	/**
	 * セリフを縦書き形式で描画
	 */
	 function show_txt_by_vertical() {
		var NARRE_TXT_PREFIX = "――― "
		var all_tag = "";

		// 行で分割
		var sc_div = $("#vertical_script_area")			
		var line_txts = get_line_txts_from_div(sc_div);

		// タイトルをセット
		var title = $('td.title').text();

		// 名前だけをセット
		var names = get_names(line_txts)

		// 行を走査
		for ( i = 0; i < line_txts.length; i++ ) {
			// 該当行のテキストをセット
			var ltxt = line_txts[i];
			// 名前をセット 
			var txts = ltxt.split("：");
			var name = txts[0]
			// セリフをセット
			var vc = txts[1]

			// 前の行の氏名をセット
			var p_name = get_line_name(line_txts, i - 1)

			// 前の行の氏名と異なれば新しい行へ
			if ( p_name != name ) {
				// 1行目以外は閉じタグを追加
				if ( i != 0 ) {
					all_tag += "</p></div>"
				}

				var ncl = get_name_class(names, name);

				all_tag += "<div class = '" + ncl + "'>"
				all_tag += "<span class = '" + ncl + "'>" + name + "</span>" + "<p class = '" + ncl + "'>"
				// ト書きの場合のみ、--を追加
				if (name == "0" || name == "") {
					vc = NARRE_TXT_PREFIX + vc
				}

				// セリフを描画
				all_tag += vc; 							
			// 前の行と氏名が同じなら改行＋セリフのみ
			} else {
				all_tag += "<br />" + vc
			}
		}

		all_tag += "</p></div>"

		// 描画
		$("#vertical_script_area").html(all_tag);
	 }

	/*======================================================
	/*
	/* 3-5. 台本詳細ページ イベント 縦書きセリフクリック
	/*
	/*======================================================*/		

	/**
	 * 縦書きセリフクリック
	 */
	$("div#vertical_script_area div").click(function() {
		// 段落のクラスを取得
		var cl = $(this).attr("class");
		var bg = rgbTo16($(this).css("background-color"));

		// 縦書き台本の段落を走査
		$('div#vertical_script_area div').each(function(i) {
			// nameセルをセット
			var tcl = $(this).attr("class");

			// クラスが一致すれば
			if (tcl == cl) {
				if (bg == "#ffffff") {
					$(this).css("background-color", "#efefef");	
				} else {
					$(this).css("background-color", "#ffffff");						
				}
			}
		});		
	});
});

```

意外といけますね。
縦書きHTMLは普段全く使わないのでかなり新鮮でした。

また、縦書き台本はセリフをクリックすると該当キャラのセリフがすべて色付けされるようにしています。
<img src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/200881/2c2f6b3b-548c-1e79-7698-53c2f911f4c0.png" width = "700">