並び替え順を記憶する

概  要

先回、jQuery UI “Sortable” の並び替え対象にローカル画像を追加する方法について検討しました。但し、並び替えた内容は同じブラウザ内では有効ですが、別ブラウザで開きなおすとリセットされてしまいます。そこで、今回はサーバー上の並び替え情報を更新する方法について検討してみます。また、リスト上に登録されている画像データを抹消する方法についても同時に検討します。

プログラム概要

下図の右上に“並び替え順序保存”ボタンを追加しました。また、各画像の行ごとに県名の下に“抹消”のラジオボタンを追加しています。画像の順番を並び替え、抹消したい画像のラジオボタンにチェックを入れた状態で、“並び替え順序保存”ボタンをクリックすると、クリック時の状態をプログラムで確認し、サーバー上のPHPプログラムに送信します。PHPプログラムは受信データ内容に応じ、関連情報(テキストファイル)更新,画像ファイル抹消を行います。
(→画像並び替え順更新・画像抹消サンプル
 FIND/47 のフリー画像を利用しています。写真はすべてクリエイティブ・コモンズ・ライセンス(表示4.0 国際)利用を許諾されています。

プログラムメモ

(1)ブラウザー側プログラム
ページ呼び出し時にPHPプログラムでテキストファイルの関連情報を読んで、HTMLに変換・出力していますが、今回は反対にJavaScriptで編集後のHTMLテキストノードを取得し、サーバー側PHPプログラムにデータを渡してテキストファイルを生成したり、抹消チェックが入った画像ファイルを削除します。
行番59で追加した“並び替え順序保存”ボタンをクリックすると、行番258~282でテーブル内画像関連情報を取得します。行番260でテーブルの並び替え領域(id=’sortable’)を取得し、次に行番261で並び替え領域内の<TR>要素を配列として取得します。この<TR>要素が並び替え単位となりますので、行番263~277のループでは、並び替え順に1つづつ関連情報を取得出来ます。
行番264では、<img>要素に設定されているファイルのフルパスを取得し、行番265で拡張子付ファイル名,行番266で拡張子無しファイル名に変換します。拡張子無しファイル名は実際には使用していないのですが、変換方法が参考になるので残しています。
県,写真タイトル,撮影者などの関連情報は、ページ呼び出し時にPHPプログラムでそれぞれ<b>タグで囲む様にしています。(行番85と行番88) その様にすることで、行番269~270で<b>要素のinnerHTMLを取得することで、関連情報を配列として同時に取得することが出来ます。行番272~274では、<input>要素ラジオボタンのチェック状態を取得し、送信データを“DEL”(抹消),もしくは “SAV”(保存)に設定しています。
実際にサーバー側に送信するデータは下の例の様なデータに加工しています。リストの上から表示順に4つの画像の関連情報を示しています。各画像毎の関連情報は、左からファイル名,県,写真タイトル,撮影者,保存/抹消情報となっています。
リスト上の全データ加工後、行番279で行番285~301のデータ送信関数を呼び出し、データをサーバー側PHPプログラムに送信します。

【サーバー側への送信データ例】

【ブラウザー側プログラム】

<!doctype html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>要素追加テスト</title>

  <style>
    body {line-height: 100%; }
    p { line-height : 0 px ; margin : 3px ; }
    table { border : solid 1px #000000 ; border-collapse: collapse; }
    th,td { border : solid 1px #000000 ; height : 250px ; text-align:center; }
    th { background-color: #99cc00 ; height : 25px ;}

    .ui-state-highlight { background-color: #ffff00; height:250px; }
    .dsp_no{ width:45px; }
    .prfctr{ width:75px; }
    .itm_nm{ width:430px; }
  </style>


  <script type="text/javascript" src="./js/jquery-3.3.1.min.js"></script>
  <script type="text/javascript" src="./js/jquery-ui-1.12.1/jquery-ui.min.js"></script>
  <script type="text/javascript" src="./js/jquery.ui.touch-punch.min.js"></script>
  <script>
    $( function() {
      	$( "#sortable" ).sortable({
        	placeholder: 'ui-state-highlight'
      	});

      	$( "#sortable" ).disableSelection();

      	$('#sortable').on('sortstop', function (e, ui) {
    		// ソートが完了したら実行される。
    		var rows = $('#sortable .dsp_no');
    		for (var i = 0, rowTotal = rows.length; rowTotal > i ; i += 1) {
        		$($('.dsp_no')[i]).text(i + 1);
    		}
      	});
    } );
  </script>
</head>


<body>

<h2>◆画像を追加します。◆</h2>

<TABLE style="border-style:none;"><TR>
<TD style="height:10px;border-style:none;"><input type="file" id="file"></TD>
<TD style="height:10px;border-style:none;width:3px;"></TD>
<TD style="height:10px;border-style:none;"><input type="text" id="add_prefec" style="width:65px; text-align:center;"></TD>
<TD style="height:10px;border-style:none;"><input type="text" id="add_ttlnam" style="width:200px; text-align:center;"></TD>
<TD style="height:10px;border-style:none;"><input type="text" id="add_psninf" style="width:200px; text-align:center;"></TD>
<TD style="height:10px;border-style:none;width:3px;"></TD>
<TD style="height:10px;border-style:none;"><input type="button" id="save_canvas" value="追加" onclick="save_canvas();"></TD>

<TD style="height:10px;border-style:none;width:3px;"></TD>
<TD style="height:10px;border-style:none;"><input type="button" id="rec_list_order" value="並び替え順序保存" onclick="rec_list_order();"></TD>
</TR></TABLE>
<div><canvas id="cvs01"></canvas></div>

<h2>◆自由に順番を入れ替えて下さい。◆</h2>
<table>
    <thead>
        <tr><th>番号</th><th>県</th><th>イメージ</th></tr>
    </thead>

    <tbody id="sortable">

	<?php
	    $img_h = 220 ;
	    $dat_tmp = file_get_contents("./add_pict/img_rcd.txt");
	    $dat_ary = explode("\n",$dat_tmp);
	    $cnt = count($dat_ary)-1;
	    for( $i=0;$i<$cnt;$i++ ) {
		$dsp_ary = explode(",",$dat_ary[$i]) ;
		$img_pth = "./add_pict/".$dsp_ary[0] ;

		$img_inf_ary = getimagesize( $img_pth );
		$img_w = floor($img_inf_ary[0] * $img_h / $img_inf_ary[1]) ;
		
		echo "<tr class=\"srt_ln\">" ;
		echo "<td class=\"dsp_no\"><STRONG>".strval($i+1)."</STRONG></td>" ;
		echo "<td class=\"prfctr\"><b>".$dsp_ary[1]."</b><BR><BR>" ;
    		echo "<input type='radio' name='".$dsp_ary[0]."' value='del' />抹消</td>" ;
		echo "<td class=\"itm_nm\"><img src=".$img_pth." alt=".$dsp_ary[2]." width=\"".strval($img_w)."\" height=\"".strval($img_h)."\" border=\"0\">";
		echo "<BR><P><b>".$dsp_ary[2]."</b> &copy <b>".$dsp_ary[3]."</b></P></td>" ;
		echo "</tr>" ;
	    }
	?>

    </tbody>
</table>

<p>写真引用元 : <a href="https://search.find47.jp" target="_blank">FIND/47</a></p>
<p>写真はすべて<a href="https://creativecommons.org/licenses/by/4.0/" target="_blank">クリエイティブ・コモンズ・ライセンス(表示4.0 国際)</a>のもと<BR>掲載を許諾されています。</p>
<div id="img_output"></div>

  <script>
	document.getElementById("file").addEventListener("change", function (e) {
	    var file = e.target.files; 
            var reader = new FileReader(); 

            reader.readAsDataURL(file[0]); 		//ファイルが複数読み込まれた際に、1つめを選択 

            //ファイルが読込完了時、処理
            reader.onload = function () { 
                var src = reader.result; 
                drawCanvas(src); 
            }; 
        }, false); 


     	function drawCanvas(source) {
	    var image = new Image(); 
            image.src = source; 

            image.onload = function () {
		var img_h = image.height ;
		var img_w = image.width ;

		var cvs_01 = document.getElementById('cvs01'); 
		if (cvs_01.getContext) { 
			var ctx_01 = cvs_01.getContext('2d'); 
		    	var cmp_h_01 = 400 ;
		    	var cmp_w_01 = Math.floor(img_w * cmp_h_01 / img_h) ;
                    	cvs_01.height = cmp_h_01;
		    	cvs_01.width = cmp_w_01;
                    	ctx_01.drawImage(image, 0 , 0 , cmp_w_01 , cmp_h_01 );
            	}
	    }
    	}

	function save_canvas(){
		var tr_cnt = document.getElementsByTagName('tr').length ;
		if(tr_cnt-2 >= 20){
			alert ("最大登録数超過! 処理を中断します。(既登録数:"+String(tr_cnt-2)+")");
			exit ;
		}

		var imageType = "image/jpeg" ;
		var tgt_cvs = "cvs01";
		var fil_nam = create_file_name() ;

		var add_inf = [] ;
		add_inf[0] = fil_nam + ".jpg" ;
		add_inf[1] = document.getElementById("add_prefec").value ;
		add_inf[2] = document.getElementById("add_ttlnam").value ;
		add_inf[3] = document.getElementById("add_psninf").value ;

		var snd_inf = add_inf[0] + "," + add_inf[1] + "," + add_inf[2] + "," + add_inf[3]  + ",SAV" ;

 		var target_cvs = document.getElementById(tgt_cvs) ;
		var cvs_h = target_cvs.height ;
		var cvs_w = target_cvs.width ;
		var base64 = target_cvs.toDataURL(imageType) ;
		var blob = Base64toBlob(base64) ;

		var formData = new FormData() ;
		formData.append( "hoge" , blob , snd_inf ) ;

		var rtn = "" ;
		var xmlhttp = createXMLHttpRequest() ;
		if( xmlhttp != null ){
		    xmlhttp.open( "POST" , "rcv_blob_add_pict.php" , false );
		    xmlhttp.send( formData ) ;

		    rtn = xmlhttp.responseText ;

		    var rtn_add = add_tr_element(add_inf[0],add_inf[1],add_inf[2],add_inf[3],cvs_w,cvs_h,tr_cnt) ;

		}else{
		    rtn = "ERROR (HTTP REQ)" ;
		}

		alert("◆アップロード結果◆\n\n" + rtn );

	}

	function Base64toBlob(base64){
	
	    var tmp = base64.split(',') ;
	    var data = atob(tmp[1]) ;
	    var mime = tmp[0].split(':')[1].split(';')[0] ;
	    var buf = new Uint8Array(data.length);

	    for (var i=0 ; data.length > i ; i++ ){
		buf[i] = data.charCodeAt(i) ;
	    }

	    var blob = new Blob( [buf] , {type:mime} ) ;
	    return blob ;
	}

	function createXMLHttpRequest(){
	    if(window.XMLHttpRequest){return new XMLHttpRequest()}
	    if(window.ActiveXObject){
		try{return new ActiveXObject("Msxml2.XMLHTTP.6.0")}catch(e){}
		try{return new ActiveXObject("Msxml2.XMLHTTP.3.0")}catch(e){}
		try{return new ActiveXObject("Microsoft.XMLHTTP")}catch(e){}
            }
	    return false;
	}

	//ファイル名を生成する
	function create_file_name(){
		var today=new Date() ;
		var dt_inf = [] ;
		dt_inf[0] = String(today.getFullYear());
		dt_inf[1] = "00"+String(today.getMonth()+1);
		dt_inf[2] = "00"+String(today.getDate());
		dt_inf[3] = "00"+String(today.getHours());
		dt_inf[4] = "00"+String(today.getMinutes());
		dt_inf[5] = "00"+String(today.getSeconds());
		dt_inf[6] = "000000000000" + String(Math.floor(100000000*Math.random())) ;
		dt_inf[6] = dt_inf[6].substr(-10,10) ;

		var dt_rtn = "" ;
		for ( var i=0 ; 6 > i ; i++ ){
			if( i != 6 ) { dt_inf[i] = dt_inf[i].substr(-2,2) ; }
			dt_rtn = dt_rtn + dt_inf[i] ;
		}

		dt_rtn = dt_rtn + "_" + dt_inf[6] ;

		return dt_rtn ;
	}

	function add_tr_element(par01,par02,par03,par04,cvs_w,cvs_h,tr_cnt){
		var tr_num = tr_cnt - 1 ;

		var dsp_num = String(tr_num) ;
		var dsp_prf = par02;
		var dsp_ttl = par03;
		var dsp_psn = par04;
		var dsp_fil = par01;

		var dsp_h = 220 ;
		var dsp_w = Math.floor(cvs_w * dsp_h / cvs_h) ;
		var str_h = String(dsp_h) ;
		var str_w = String(dsp_w) ;

		var ins_html = '<td class="dsp_no"><STRONG>'+dsp_num+'</STRONG></td>';
		ins_html = ins_html + '<td class="prfctr"><b>'+dsp_prf+'</b><BR><BR>' ;
		ins_html = ins_html + '<input type="radio" name="'+dsp_fil+'" value="del" />抹消</td>' ;
		ins_html = ins_html + '<td class="itm_nm"><img src="./add_pict/'+dsp_fil+'" alt='+dsp_ttl+' width="'+str_w+'" height="'+str_h+'" border="0">' ;
		ins_html = ins_html + '<BR><P><b>'+dsp_ttl+'</b> &copy <b>'+dsp_psn+'</b></P></td>' ;

		var tr_element = document.createElement("tr") ;
		tr_element.className = "srt_ln" ;
		tr_element.innerHTML = ins_html ;

		var parent_object = document.getElementById("sortable") ;
    		parent_object.appendChild(tr_element);
	}

	function rec_list_order(){
		var list_order = "" ;
		var srt_tgt = document.getElementById('sortable') ;
		var tr_Node = srt_tgt.getElementsByTagName('tr') ;

		for (var i = 0; i < tr_Node.length; i++) {
		    var pth_f = tr_Node[i].getElementsByTagName('img')[0].src ;			//  ファイルパス
		    var fl_nm_ex = pth_f.match(".+/(.+?)([\?#;].*)?$")[1] ;			// ◆ファイル名(拡張子有)
		    var fl_nm_no = pth_f.match(".+/(.+?)\.[a-z]+([\?#;].*)?$")[1] ;		//  ファイル名(拡張子無)

		    var tmp_b = [] ;
		    var img_cmt = tr_Node[i].getElementsByTagName('b') ;			//  県・タイトル・作成者
		    for (var j = 0; j < img_cmt.length; j++) { tmp_b[j] = img_cmt[j].innerHTML ; }

		    var opt_del = tr_Node[i].getElementsByTagName('input') ;			//  削除(ラジオボックスチェック)
		    
		    if(opt_del[0].checked){ tmp_b[3]="DEL"; } else { tmp_b[3]="SAV"; }

		    list_order = list_order + fl_nm_ex+","+tmp_b[0]+","+tmp_b[1]+","+tmp_b[2]+","+tmp_b[3]+"<BR>" ;
		}

		var res_snd = send_list_order(list_order) ;

		alert("リスト並び替え処理完了");
	}


	function send_list_order(snd_dat){
		var formData = new FormData() ;
		formData.append( "lsinf" , snd_dat ) ;

		var rtn = "" ;
		var xmlhttp = createXMLHttpRequest() ;

		if( xmlhttp != null ){
		    xmlhttp.open( "POST" , "rcv_list_info.php" , false );	// 選択項目抹消
		    xmlhttp.send( formData ) ;
		    rtn = xmlhttp.responseText ;

		}else{
		    rtn = "ERROR (HTTP REQUEST)" ;
		}
		return rtn ;
	}

  </script>

</body>
</html>


(2)サーバー側PHPプログラム
サーバー側PHPプログラムの処理についてメモしておきます。行番3で送信データを取得します。行番5~6で取得データを行毎に分割し配列に格納します。行番10~20のループ処理で1行分づつデータを処理していきます。行番12ではカンマ(’ , ‘)でデータを分割し、配列に ファイル名,県,写真タイトル,撮影者,保存/抹消の順にデータを格納します。
行番13では、保存/抹消区分が、“SAV”(保存)/ “DEL”(抹消)の判定を行い、“SAV”(保存)の場合は、行番15で変数“$out_str”にデータを追加し、 “DEL”(抹消)の場合は、データ追加は行わず、行番17で画像ファイルを抹消します。この様にして保存対象のデータのみを追加し、行番22でサーバー側の関連情報ファイルを更新します。

<?php

$tmp_str = $_POST["lsinf"] ;

$tmp_str = str_replace("<BR>","\n", $tmp_str) ;
$tmp_ary = explode("\n", $tmp_str) ;
$out_str = "" ;
$lop_cnt = 0 ;

for($i=0;$i<count($tmp_ary);$i++){
    if(mb_strlen($tmp_ary[$i]) > 3){
	$itm_ary=explode(",",$tmp_ary[$i]);
	if($itm_ary[4]=="SAV"){
	    $lop_cnt=$lop_cnt+1 ;
	    $out_str=$out_str.$tmp_ary[$i]."\n" ;
	}else{
	    unlink("./add_pict/".$itm_ary[0]) ;
	}
    }
}

file_put_contents("./add_pict/img_rcd.txt" , $out_str);

echo "OK" ;

?>
まとめ

写真リストを簡単にスクロールさせたり、全体の外観を良くしたり、リストの中の写真を選んで拡大させたりしたいなどと思ったりもしますが、“まぁ、こんなところでしょう。”と自分を納得させて、とりあえず今回は終了します。