これまではひとつのチャートしか表示させることができませんでした。今回はひとつのアプリ内に複数のチャートを表示できるようにします。
⇒ 動作確認はこちらから
いったんトップページに戻って新規ボタンを押すと追加できます。
トップページ ガントチャートの一覧が並びます。
/page-(整数) ガントチャートが表示されます。
/edit-(整数) タスクを編集するページです。
Contents
トップページと新しいチャートをつくるページ
これまでつくったものを大幅に変更します。複数のページを動的に生成するために.htaccessを作成してサーバーのどこかに設置します。実際に公開するまえにローカルでテストをしてください。
.htaccess
1 2 |
RewriteEngine on RewriteRule ^(.*)$ index.php?arg=$1 [QSA,L] |
これで設置したurlにアクセスするとそれ以下の部分が引数としてindex.phpに渡されます。これで複数のページを表示させることができます。http://example.jp/test/で公開するのであれば、http://example.jp/test/task/1にアクセスすれば引数は「1」、http://example.jp/test/task/1/であれば「1/」になります。階層が異なるページをつくるとややこしくなるのでページはすべて同じ階層とし、そうでない場合は不正なアクセスと見なします。
index.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
<?php $table_pages = "table_pages"; $table_tasks = 'table_tasks'; $db_path = './tasks.sqlite3'; main(); function main(){ try { $arg = $_GET["arg"]; if($arg == ""){ ShowTopPage(); return; } else { // 引数チェック if($arg == "new"){ ShowNewPage(); return; } else if($arg == "post-from-new"){ PostFromNewPage(); return; } else if(strpos($arg, 'page-') === 0) { $id = str_replace('page-',"", $arg); $id = (int) $id; if($id > 0){ // 引数がpage-(1以上の整数)の場合は作成されたチャートを表示する ShowPage($id); return; } else{ echo '不正なリクエストです'; return; } } else if(strpos($arg, 'edit-') === 0) { $id = str_replace('edit-',"", $arg); if($id > 0){ // 引数がedit-(1以上の整数)の場合は編集用のページを表示する ShowEditPage($id); return; } else{ echo '不正なリクエストです'; return; } } else if(strpos($arg, 'update-') === 0) { $id = str_replace('update-',"", $arg); if($id > 0){ // POSTで引数がupdate-(1以上の整数)の場合は更新処理をする PostToUpdate($id); return; } else{ echo '不正なリクエストです'; return; } } else if(strpos($arg, 'delete-') === 0) { // POSTで引数がdelete-(1以上の整数)の場合はページの削除の処理をする $id = str_replace('delete-',"", $arg); $id = (int) $id; if($id > 0){ PostToDelete($id); return; } else{ echo '不正なリクエストです'; return; } } echo $arg . 'は不正なリクエストです'; return; } } catch(Exception $e) { echo '例外<br>'; } } function ShowTopPage(){} function ShowNewPage(){} function PostFromNewPage(){} function ShowPage($id){} function ShowEditPage($id){} function PostToUpdate($id){} function PostToDelete($id){} |
新規ボタンをクリックしたら新しいチャートを作成するために/newにリダイレクトします。
template/index.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>ガントチャートの実験</title> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <style> #container { width: 800px; margin: auto; } </style> </head> <body> <div id ="container"> <button class = "button" onclick="CreateNew()">新規</button> <div id = "papes"> {PAGES} </div> </div> <script> function CreateNew(){ window.location.href = `./new`; } </script> </body> </html> |
次に新しいチャートを作成するページを表示させる処理を示します。これはHTMLです。
template/new.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>新規</title> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> </head> <body> <form action="./post-from-new" method="post"> <label>新規チャート名:</label> <input id = "chart-name" name = "chart-name" type="text" size="24" required="required"> <button style="margin-left: 20px;">チャートの追加</button> </form> </body> </html> |
新しいテーブルをつくる
ページ情報を保存するための新しいテーブルをつくります。そのための関数を示します。
index.php
1 2 3 4 5 6 7 8 9 10 |
function CreatePagesTable($db){ global $table_pages; $text = "CREATE TABLE if not exists {$table_pages} ("; $text .= "id integer primary key autoincrement,"; $text .= "PageName text"; $text .= ");"; $sql = $db->prepare($text); $sql->execute(); } |
各ページへのリンクを生成し表示する
トップページにアクセスされたときの処理を示します。
$table_pagesを調べてデータがあれば各チャートへのリンクを生成します。
index.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
function ShowTopPage(){ global $table_tasks, $db_path; global $table_pages; $html = file_get_contents("./template/index.html"); $db = new PDO("sqlite:{$db_path}"); CreatePagesTable($db); $pages = $db->prepare("SELECT * FROM {$table_pages}"); $pages->execute(); $result = $pages->fetchAll(PDO::FETCH_ASSOC); $pages = null; $pagesHtml = ""; $size = count($result); for($i = 0; $i<$size; $i++) { $ret = $result[$i]; $id = $ret["id"]; $pageTitle = $ret["PageName"]; $link = "./{$id}"; $pagesHtml .= '<p><a href="./page-' . strval($id) . '">' . $pageTitle . '</a></p>' . "\n"; } $html = str_replace('{PAGES}',$pagesHtml, $html); echo $html; $db = null; } |
チャートの追加ボタンをクリックしたときの処理
/newにアクセスしたときの処理を示します。template/new.htmlを読み込んで表示するだけです。
index.php
1 2 3 4 |
function ShowNewPage(){ $newhtml = file_get_contents("./template/new.html"); echo $newhtml; } |
/newでチャートの追加ボタンをクリックしたときの処理を示します。/post-from-newへPostされたときの処理を示します。
SqliteのDBにテーブル $table_pagesがないなら作成して新しいチャート名を保存します。そして新しく作成したページにリダイレクトします。
index.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
function PostFromNewPage(){ global $db_path, $table_pages; $db = new PDO("sqlite:{$db_path}"); $chartName = $_POST["chart-name"]; $chartName = EscapeString($chartName); CreatePagesTable($db); $sql = $db->prepare("INSERT INTO {$table_pages} (PageName) VALUES (?)"); $sql->execute([$chartName]); $text = "select max(id) from {$table_pages}"; $sql = $db->prepare($text); $sql->execute(); $result = $sql->fetch(PDO::FETCH_ASSOC); // 新しく登録されたデータのidを調べる $max = (int)$result["max(id)"]; $db = null; // 新しく生成されたページへリダイレクトする $next = "./page-" . $max; header("Location: $next"); } |
各ページにアクセスされたときの処理
各チャートのページにアクセスされたときの処理を示します。
まずHTMLですが、これを読み取ってJavaScriptのコード部分を置き換えます。
template/page.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>{PAGENAME} - ガントチャートの実験</title> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <style> .controller { margin-bottom: 20px; font-weight: bold; } #year-month { margin-bottom: 30px; } #chart-name { font-weight: bold; } .button { width: 100px; margin-right: 30px; } #edit-button { width: 150px; margin-left: 100px;} #go-top-button { float: right; margin-right: 100px; } </style> </head> <body> <div class = "controller"> <div id = "chart-name">{PAGENAME}</div> <div id = "year-month"></div> <button class = "button" onclick="Prev()">前月</button> <button class = "button" onclick="Next()">次月</button> <button id = "edit-button" onclick="GoEditTask()">タスクを編集する</button> <button id = "go-top-button" onclick="GoTop()">トップページへ</button> </div> <div id = "main"></div> <script> {GANTT_CHART_JS} // これまでに作成したJavaScriptのコードを取得して置き換える {GANTT_CHART2_JS} let main_element = document.getElementById('main'); let ganttChart = new GanttChart(main_element); ShowYearMonth(); ShowChart(); ShowSchedules(); function ShowSchedules(){ let tasks = []; {SCRIPT} ganttChart.SetSchedules(tasks); } function GoTop(){ location.href = './'; } </script> </body> </html> |
次にPHP部分を示します。
これはid からタスク情報が保存されているテーブル名を取得する関数です。
index.php
1 2 3 4 5 6 7 |
function GetTaskTableName($id){ global $table_tasks; if($id == null) return $table_tasks; else return "{$table_tasks}_{$id}"; } |
これはHTMLの{SCRIPT}と書かれている部分に入るJavaScriptのコードを取得するための関数です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
function GetScriptShowPage($db, $id){ $tableNameTasks = GetTaskTableName($id); $tasks = $db->prepare("SELECT * FROM {$tableNameTasks}"); $tasks->execute(); $result = $tasks->fetchAll(PDO::FETCH_ASSOC); $scriptText = ''; foreach ($result as $row) { $start = $row["StartDateText"]; $end = $row["EndDateText"]; if($start == "") continue; $starts = explode('-', $start); $ends = explode('-', $end); $taskName = $row["TaskName"]; $categoryName = $row["CategoryName"]; $managerName = $row["ManagerName"]; $scriptText .= "tasks.push(new Task('{$taskName}', '{$categoryName}', '{$managerName}', "; $scriptText .= "{$starts[0]}, {$starts[1]}, {$starts[2]}, "; $scriptText .= "{$ends[0]}, {$ends[1]}, {$ends[2]}));"; $scriptText .= "\n"; } return $scriptText; } |
これはページを表示するための関数です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
function ShowPage($id){ if(strpos($id, '/') !== false){ echo '不正なリクエストです'; return; } if(isset($_SESSION["hash_pass_{$id}"])) unset($_SESSION["hash_pass_{$id}"]); global $db_path, $table_pages; $html = file_get_contents("./template/page.html"); $db = new PDO("sqlite:{$db_path}"); CreateTaskTable($db, $id); $pages = $db->prepare("SELECT * FROM {$table_pages} where id = {$id}"); $pages->execute(); $result = $pages->fetch(PDO::FETCH_ASSOC); if($result == null){ echo "ページが存在しません"; $db = null; return; } $pageTitle = $result["PageName"]; $scriptText = GetScriptShowPage($db, $id); $db = null; $js = file_get_contents("./js/gantt-chart.js"); $js2 = file_get_contents("./js/gantt-chart2.js"); $html = str_replace('{GANTT_CHART_JS}', $js, $html); // 置換実行 $html = str_replace('{GANTT_CHART2_JS}', $js2, $html); $html = str_replace('{SCRIPT}', $scriptText, $html); $html = str_replace('{PAGENAME}', $pageTitle, $html); echo $html; } |
タスクを編集するボタンがクリックされたときの処理
タスクを編集するボタンがクリックされたときの処理を示します。ドメイントップに設置された場合以外でもうまくリダイレクトできるようにlocation.pathnameからidを取り出せるようにしています。urlの構造から最後の-より後がidです。
js/gantt-chart2.js
1 2 3 4 5 6 7 |
function GoEditTask(){ // '/XXX/page-XX' を '/XXX/edit-XX' へ let pathame = location.pathname; let last = pathame.lastIndexOf('-'); let id = pathame.substring(last +1); window.location.href = './edit-' + id; } |
タスクを編集するページにアクセスされたときの処理
タスクを編集するページ /edit-XXにアクセスしたときの処理を示します。
これはHTMLです。
edit.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>編集</title> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <style> #data { display: none; } /* 非表示 */ #chart-name-label {margin-left: 20px; } #chart-name {margin-right: 20px; } #add-category {margin-right: 20px; } #to-reflect {margin-right: 20px; } #delete-chart { float: right; } </style> </head> <body> <p id="data">{DATA}</p> <label id = "chart-name-label" style="cursor: move;">チャート名:</label> <input id = "chart-name" type="text" size="24" value = "{PAGE_TITLE}"> <button id = "add-category" onclick="AddCategory()">カテゴリ追加</button> <button id = "to-reflect" onclick="ToReflect()">反映</button> <button id = "delete-chart" onclick="DeleteChart()">チャートを削除</button> <div id="categorys"></div> <div id="popup-calendar"> <div id="close-btn">?</div> <div id="next-prev-button"> <button id="prev" onclick="Prev()">前月</button> <button id="next" onclick="Next()">次月</button> </div> <h1 id="calendar-header"></h1> <div id="calendar"></div> </div> <script> {MINI-CALENDAR_JS} {TASK1_JS} {TASK2_JS} {TASK3_JS} </script> </body> </html> |
反映ボタンが押されたら要素を調べてSQLiteのDBに保存します。
クライアント側の処理
クライアント側の処理を示します。
ToReflect関数を書き直します。Postする対象が前回とは違っています。それからテキストボックスに書かれている文字列が新しいページタイトルになるので、これもPostします。この部分は新しいGetResultJsonText関数を参照してください。
task2.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
function ToReflect(){ // Postするurlを求める /XXX/edit-XX から /XXX/update-XX へpostする let pathame = location.pathname; let last = pathame.lastIndexOf('-'); let id = pathame.substring(last +1); let post = `./update-${id}`; xhr.open('POST', `${post}`, true); xhr.setRequestHeader('content-type', 'application/x-www-form-urlencoded;charset=UTF-8'); xhr.send(GetResultJsonText()); // Postしたら /XXX/page-XX へリダイレクトする xhr.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200){ window.location.href = `./page-${id}`; } }; } |
GetResultJsonText関数も書き換えとなります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
function GetResultJsonText(){ let categoryElements = GetCategoryElements(); let obj = { Tasks : [], // チャートの名前(ページタイトル)も渡せるようにする(実は変更点ここだけです) PageName : document.getElementById('chart-name').value, }; categoryElements.forEach(categoryElement => { let taskElements = GetTaskElements(categoryElement); let isEmpty = true; taskElements.forEach(taskElement => { isEmpty = false; let task = { TaskName : GetTaskNameFromElement(taskElement), CategoryName : GetCategoryNameFromElement(categoryElement), ManagerName : GetManagerNameFromElement(taskElement), StartDateText: GetStartDateFromElement(taskElement), EndDateText: GetEndDateFromElement(taskElement), }; obj.Tasks.push(task); }); // カテゴリにタスクがひとつも存在しない場合の処理は前回と同じ if(isEmpty == true){ let task = { TaskName : '', CategoryName : GetCategoryNameFromElement(categoryElement), ManagerName : '', StartDateText: '', EndDateText: '', }; obj.Tasks.push(task); } }); return JSON.stringify(obj); } |
サーバーサイドの処理
つぎにindex.phpにおける処理を示します。
/pageedit-XXにアクセスされたときの処理を示します。SQLiteのDBにデータが登録されているか調べて、存在する場合はページタイトルと編集すべきタスクを表示させます。登録されていない場合は存在しないページである旨示すエラーページを表示させます。
index.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
function ShowEditPage($id){ global $table_tasks, $db_path; global $table_pages; $html = file_get_contents("./template/edit.html"); $db = new PDO("sqlite:{$db_path}"); CreatePagesTable($db); $sql = $db->prepare("SELECT * FROM {$table_pages} where id = {$id}"); $sql->execute(); $result = $sql->fetch(PDO::FETCH_ASSOC); if($result == null){ $db = null; echo 'ページが存在しません'; return; } $pageTitle = $result["PageName"]; CreateTaskTable($db, $id); $taskTableName = GetTaskTableName($id); $sql = $db->prepare("SELECT * FROM {$taskTableName}"); $sql->execute(); $result = $sql->fetchAll(PDO::FETCH_ASSOC); $jsonText = json_encode($result, JSON_UNESCAPED_UNICODE); $js = file_get_contents("./js/mini-calendar.js"); $js1 = file_get_contents("./js/task1.js"); $js2 = file_get_contents("./js/task2.js"); $js3 = file_get_contents("./js/task3.js"); $html = str_replace("{MINI-CALENDAR_JS}", $js, $html); $html = str_replace("{TASK1_JS}", $js1, $html); $html = str_replace("{TASK2_JS}", $js2, $html); $html = str_replace("{TASK3_JS}", $js3, $html); $html = str_replace("{PAGE_TITLE}", $pageTitle, $html); $html = str_replace("{DATA}", $jsonText, $html); echo $html; $db = null; } |
Postされたときの処理
/edit-XXで反映ボタンが押されて/update-XXへPostされた場合の処理を示します。
いったんテーブルを削除して作り直していますが、ここでちょっと注意が必要です。このページのurlを直接入力してもこの関数を実行することができてしまいます。そこで悪意のあるユーザーによってデータを書き換えられないためにチェックが必要です。サイト内(同一ドメイン)からのアクセスであることを確認したほうがよいと思われます。
タスクを編集するページのテキストボックスに書かれている文字列が新しいページタイトルになります。Postされたときにこれが送られてくるので、これもSQLiteのDBに保存できるようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
function PostToUpdate($id) { $json = file_get_contents("php://input"); // JSON文字列をobjectに変換 ⇒ 第2引数をtrueにしないとハマるので注意 // objectに変換したうえで$contents["Tasks"]が存在しない場合などは不正なアクセスであると考えられる $contents = json_decode($json, true); if($contents == null || $contents["Tasks"] == null){ echo '不正なリクエストです'; return; } // 同一ドメインからのアクセスであることを確認すべき $addr = $_SERVER['REMOTE_ADDR']; $host = strtolower($addr); $hostname = gethostbyaddr($host); if($hostname != '自分のドメイン'){ echo '不正なリクエストです'; return; } global $table_tasks, $db_path; global $table_pages; $db = new PDO("sqlite:{$db_path}"); // 存在するページなのか? CreatePagesTable($db); $sql = $db->prepare("SELECT * FROM {$table_pages} where id = {$id}"); $sql->execute(); $result = $sql->fetch(PDO::FETCH_ASSOC); if($result == null){ $db = null; echo 'ページが存在しません'; return; } $taskTableName = GetTaskTableName($id); // テーブルはその都度作り直す $sql = $db->prepare("drop table if exists {$taskTableName}"); $sql->execute(); CreateTaskTable($db, $id); foreach ($contents["Tasks"] as $content) { $categoryName = $content["CategoryName"]; $categoryName = EscapeString($categoryName); $taskName = $content["TaskName"]; $taskName = EscapeString($taskName); $managerName = $content["ManagerName"]; $managerName = EscapeString($managerName); $startDateText = $content["StartDateText"]; $endDateText = $content["EndDateText"]; $sql = $db->prepare("INSERT INTO {$taskTableName} (CategoryName,TaskName,ManagerName,StartDateText,EndDateText) VALUES (?,?,?,?,?)"); $sql->execute([$categoryName, $taskName, $managerName, $startDateText, $endDateText]); $sql = null; } // チャート名が変更されているかもしれないので変更する $pageName = EscapeString($contents["PageName"]); $sql = $db->prepare("update {$table_pages} set PageName = '{$pageName}' where id = ${id}"); $sql->execute(); $db = null; } |