【Javascript】多段階アンドゥの実装覚書2

#きっかけ
前回書いた記事
[【Javascript】多段階アンドゥの実装覚書](https://qiita.com/mushroominger/items/9c87bb6296a0c1852ef3)
のアプリ(?)をさらにバージョンアップさせたので誰かの役にたつかもって書いてみた

#アンドゥだけじゃない
間違えてページ遷移しちゃった場合も取り返せる様にローカルストレージにもチャレンジ！
今度はRedoも入れたいかな…

#コード
前より多機能にしたぶんよりわけがわからないので全文です

###html

```html:index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="workSheet.css">
  <title>ベージ遷移後の回復？実装</title>
</head>
<body>
  <div class="table">
    <!-- 区切り文字・開始番号 -->
    <form id="punctuationAndStartNum" class="tableRow">
      <span class="tableCell textAlignRight">セルの区切りを選択：</span>
      <span class="tableCell">
        <select id="punctuation">
          <option value="	">タブ区切り</option>
          <option value=",">カンマ区切り</option>
          <option value=" ">半角空白区切り</option>
        </select>
        <span class="textAlignRight">開始番号：</span>
        <input id="startNum" type="text" size="4" autocomplete="off" value="1">
      </span>
    </form>
    <!-- 区切り文字・開始番号 end -->
    <!-- 性別 -->
    <form class="tableRow">
      <span class="tableCell textAlignRight">性別を選択：</span>
      <select id="sex" class="tableCell" autofocus>
        <option value="男">男</option>
        <option value="女">女</option>
        <option value="記載無し">記載無し</option>
        <option value="質問項目無し">質問項目無し</option>
      </select>
    </form>
    <!-- 性別 end -->
    <!-- 年代 -->
    <form class="tableRow">
      <span class="tableCell textAlignRight">年代を選択：</span>
      <select id="age" class="tableCell">
        <option value="２０代">２０代</option>
        <option value="３０代">３０代</option>
        <option value="４０代">４０代</option>
        <option value="５０代">５０代</option>
        <option value="６０代">６０代</option>
        <option value="７０代〜">７０代〜</option>
        <option value="記載無し">記載無し</option>
        <option value="質問項目無し">質問項目無し</option>
      </select>
    </form>
    <!-- 年代 end -->
    <!-- 知ったきっかけ -->
    <form class="tableRow">
      <span class="tableCell textAlignRight">知ったきっかけ：</span>
      <select id="trigger" class="tableCell">
        <option value="公式ウェブサイト">公式ウェブサイト</option>
        <option value="Facebook">Facebook</option>
        <option value="twitter">twitter</option>
        <option value="知人からの紹介">知人からの紹介</option>
        <option value="メール">メール</option>
        <option value="ブログ">ブログ</option>
        <option value="お知らせハガキ">お知らせハガキ</option>
        <option value="その他">その他</option>
        <option value="記載無し">記載無し</option>
        <option value="質問項目無し">質問項目無し</option>
      </select>
    </form>
    <!-- 知ったきっかけ end -->
    <!-- その他の理由 -->
    <form class="tableRow">
      <span class="tableCell textAlignRight">その他の理由：</span>
      <input id="other" class="tableCell" type="text" autocomplete="off" placeholder="その他の理由を記入してください">
      <input type="text" size="50" autocomplete="off" placeholder="見えないハス" style="display: none;"><!-- 改行入力無効化の為 -->
    </form>
    <!-- その他の理由 end -->
    <!-- 問い合わせ内容 -->
    <form class="tableRow">
      <span class="tableCell textAlignRight vaTop">問い合わせ内容：</span>
      <textarea id="inquiry" class="tableCell" name="name" rows="8" placeholder="問い合わせ内容を記入してください"></textarea>
    </form>
    <!-- 問い合わせ内容 end -->
    <!-- 備考欄 -->
    <form class="tableRow">
      <span class="tableCell textAlignRight vaTop">備考欄：</span>
      <textarea id="nb" class="tableCell" name="name" rows="3" placeholder="備考を記入してください"></textarea>
    </form>
    <!-- 備考欄 end -->
    <!-- 入力・取消・コピー・削除ボタン -->
    <form class="tableRow">
      <span class="tableCell textAlignRight"></span>
      <div class="tableCell">
        <input id="inputButton" class="outputButton" type="button" value="入力" onclick="main();">
        <input id="deleteButton" class="outputButton" type="button" value="最終行削除" onclick="oneStepBack();">
        <input id="excelTableButton" class="outputButton" type="button" value="Excel出力用テーブル表示" onclick="excelTableShowHide()">
      </div>
    </form>
    <!-- 入力・取消・コピー・削除ボタン end -->
  </div> <!-- テーブル end -->
  <!-- 出力 -->
  <div>
    <div>
      <form id="makedTable">
        <table>
          <tr>
            <th>修正</th>
            <th>番号</th>
            <th>性別</th>
            <th>年代</th>
            <th>きっかけ</th>
            <th>その他理由</th>
            <th>問い合わせ内容</th>
            <th>備考</th>
          </tr>
        </table>
      </div>
    </form>
    <div id="excelTableShowHide" style="display: none;">
      <div id="excelTable">
        <table>
          <tr>
            <th>番号</th>
            <th>性別</th>
            <th>年代</th>
            <th>きっかけ</th>
            <th>その他理由</th>
            <th>問い合わせ内容</th>
            <th>備考</th>
          </tr>
        </table>
      </div>
    </div>
    <div>
      <form>
        <input class="outputButton" type="button" value="Text Copy" onclick="copyTextToClipboard(fullText);">
        <input id="dl-xlsx" class="outputButton" type="button" value="Download XLSX">
        <input class="outputButton" type="button" value="完全削除" onclick="removeText();">
        <input id="localStorageConfirmationButton" class="outputButton" type="button" value="ローカルストレージから復元" style="display: none;" onclick="localStorageConfirmation();">
      </form>
    </div>
  </div>
  <!-- 出力 end -->

  <script src="reload.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.9.10/xlsx.full.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/1.3.3/FileSaver.min.js"></script>
  <script src="export-xlsx.js"></script>
  <script src="workSheet.js"></script>
</body>
</html>

```

###css

```css:workSheet.css
@charset "UTF-8";

* {
  margin: 5px;
  font-size: 15px;
}

span {
  white-space: nowrap;
}

span.space {
  display: inline-block;
  width: 90px;
  margin: 0 5px;
  padding: 0;
}

input, textarea {
  border: solid 1px gray;
  border-radius: 2px;
  background: #eee;
}

textarea {
  width: 423px;
}

#other {
  width: 423px;
}

table, tr {
  margin: 0;
  padding: 0;
  border-collapse: collapse;
  border-spacing: 0;
}

th, td {
  min-width: 2em;
  margin: 0;
  padding: 5px;
  border: solid 1px gray;
  text-align: center;
}

#startNum {
  text-align: center;
}

#inputButton {
  background: #ff9872;
}

#excelTableButton {
  width: 199px;
}

.table {
  display: table;
}

.tableRow {
  display: table-row;
}

.tableCell {
  display: table-cell;
  box-sizing: border-box;
  /* border: solid 1px green; */
}

.textAlignRight {
  text-align: right;
  font-weight: bold;
}

.vaTop {
  padding-top: 7px;
  vertical-align: top;
}

.outputButton {
  height: 35px;
  padding: 5px;
  border: solid 1px black;
  border-radius: 5px;
  font-size: 15px;
}

.radioHead {
  padding: 0;
  box-sizing: border-box;
}

.radioLabel:first-child {
  display: block;
  position: relative;
  width: 100%;
  height: 100%;
  margin: 0;
  box-sizing: border-box;
  content: "";
}

```


###JS

```javascript:workSheet.js
const punctuationAndStartNum = document.getElementById('punctuationAndStartNum'); // 区切り文字と開始番号のID
const makedTable = document.getElementById('makedTable'); // テーブル作成場所のID
const makedExcelTable = document.getElementById('excelTable'); // 非表示 Excel出力用テーブルID
const startNum = document.getElementById('startNum'); // 開始番号の格納されたinput ID

// アンケート内容の場所を取得
const sex = document.getElementById('sex'); // 性別
const age = document.getElementById('age'); // 年齢
const trigger = document.getElementById('trigger'); // きっかけ
const other = document.getElementById('other'); // その他理由
const inquiry = document.getElementById('inquiry'); // 質問項目
const nb = document.getElementById('nb'); // 備考

// 初期化
var sexValue = ''; // 性別
var ageValue = ''; // 年齢
var triggerValue = ''; // きっかけ
var otherValue = ''; // その他理由
var inquiryValue = ''; // 質問項目
var inquiryValueDQ = ''; // "質問項目"
var nbValue = ''; // 備考
var nbValueDQ = ''; // "備考"

// 主なボタンのID
const inputButtonId = document.getElementById('inputButton'); // 入力ボタンのID
var inputButton = inputButtonId.value;
const deleteButtonId = document.getElementById('deleteButton'); // 削除ボタンのID
var deleteButton = deleteButtonId.value;
const localStorageConfirmationId = document.getElementById('localStorageConfirmationButton'); // 復元ボタンのID

// カウンターまわり
var count = 0; // か、カウンター 命
var excelNum = 0; // 開始番号固定
var oneStepBackNum = 0; // 削除可能か確認用
var confirmationNum = 0; // タブ変換時のアラート確認用
var fixGNum = 0; // 修正用グローバル変数

// テキスト処理関連
var textArray = [[]]; // 0コピペ用 1テーブル変換用 2編集テーブル用 3Excel出力テーブル用
var fullText = []; // コピペ用のテキスト
var json = ''; // ローカルストレージ保管用文字列
var jsonArray = []; // ローカルストレージ保管用に変換する変数
var jsonNum = 0; // ローカルストレージより取り出した開始番号

// テーブル関連のタグ格納
const tags0 = '<tr><th>番号</th><th>性別</th><th>年代</th><th>きっかけ</th><th>その他理由</th><th>問い合わせ内容</th><th>備考</th></tr>'
const tags1 = '<tr><th>修正</th><th>番号</th><th>性別</th><th>年代</th><th>きっかけ</th><th>その他理由</th><th>問い合わせ内容</th><th>備考</th></tr>'
var tableText = ['<table>' + tags1, , '</table>'];
var excelTable = ['<table  class="table-to-export" data-sheet-name="アンケートExcel">' + tags0, , '</table>']

// 読込完了時にローカルストレージを確認しボタンの表示を決める
window.addEventListener('DOMContentLoaded', function() {
  let getjson = localStorage.getItem('key');
  let obj = JSON.parse(getjson);
  if(obj != null) {
    localStorageConfirmationId.style.display = 'inline';
    if(confirm('ローカルストレージにファイルがあります\n読み込みますか？')) {
      localStorageConfirmation();
    }
  }
});

// 多分主に使われる関数 入力・修正
function main() {
  // カウンターまわりのログ確認用
  // console.log('main() start');
  // console.log('count ' + count);
  // console.log('excelNum ' + excelNum);
  // console.log('oneStepBackNum ' + oneStepBackNum);
  // console.log('confirmationNum ' + confirmationNum);
  // console.log('fixGNum ' + fixGNum);

  inputButton = inputButtonId.value;
  const punctuationId = document.getElementById('punctuation');
  const punctuation = punctuationId.value;

  if(jsonNum != 0) {
    console.log('jsonNum');
  }
  else {
    excelNum = startNum.value * 1;
    console.log('startNum');
  }

  var joined = '';
  var joinedExcel = '';
  confirmationNum = 0;

  sexValue = sex.value;

  ageValue = age.value;

  triggerValue = trigger.value;

  otherValue = other.value.replace(/ /g, '　'); // 半角スペースを全角にする。

  inquiryValue = inquiry.value.replace(/ /g, '　'); // 半角スペースを全角にする。
  inquiryValueDQ = ('"' + inquiryValue + '"');

  nbValue = nb.value.replace(/ /g, '　'); // 半角スペースを全角にする。
  nbValueDQ = ('"' + nbValue + '"');

  if(triggerValue === 'その他' && otherValue === '') {
    alert('その他の理由を入力してください');
    other.focus();
    return;
  } else if(triggerValue != 'その他' && otherValue != '') {
    alert('"知ったきっかけ"または\n"その他の理由"をみなおしてください');
    other.focus();
    return;
  }

  otherValue = tabCheck(otherValue);
  inquiryValueDQ = tabCheck(inquiryValueDQ);
  nbValueDQ = tabCheck(nbValueDQ);
  inquiryValue = tabCheck(inquiryValue);
  nbValue = tabCheck(nbValue)

  if(otherValue === false || inquiryValueDQ === false || nbValueDQ === false) {
    return;
  }

  if(inputButton === '入力') {

    substitution(count, punctuation);

    count++;

    rendering();

    punctuationAndStartNum.style.display = 'none';

    oneStepBackNum = 1;
  }

  else if(inputButton === '修正') {

    substitution(fixGNum, punctuation);
    rendering();

    inputButtonId.value = '入力';
    deleteButtonId.value = '最終行削除';
  }

  else {
    console.log('error');
  }

  localStorageConfirmationId.style.display = 'none';

} // main end

// 整理代入…
function substitution(subNum, punctuation) {
  textArray[subNum] = [];
  joined = sexValue + punctuation + ageValue + punctuation + triggerValue + punctuation + otherValue + punctuation + inquiryValueDQ + punctuation + nbValueDQ;
  joinedExcel = sexValue + punctuation + ageValue + punctuation + triggerValue + punctuation + otherValue + punctuation + inquiryValue + punctuation + nbValue;
  textArray[subNum].push(joined); // 0
  textArray[subNum].push(joinedExcel); // 1
  textArray[subNum].push(tableMake(textArray, punctuation, subNum, 1)); // 2 ラジオボタン付き
  textArray[subNum].push(tableMake(textArray, punctuation, subNum, 0)); // 3
} // substitution end

// テーブル作成・jsonに出力
function rendering() {
  tableText[1] = [];
  for(let i = (textArray.length - 1); i >= 0; i--) {
    tableText[1].push(textArray[i][2]);
  }

  excelTable[1] = [];
  fullText = [];
  for(let i = 0; i < textArray.length; i++) {
    excelTable[1].push(textArray[i][3]);

    fullText.push(textArray[i][0] + '\n');
  }

  tableText[1] = tableText[1].join('');

  excelTable[1] = excelTable[1].join('');

  fullText = fullText.join('');

  if(textArray.length != 0) {
    jsonArray = []
    for(let i = 0; i < textArray.length; i++) {
      var jsonText = textArray[i][0].replace(/\t/g, '\\t').replace(/\n/g, '\\n').replace(/"/g, "'");
      if(i === 0 && i === (textArray.length - 1)) {
        jsonArray.push('[["' + jsonText + '"]]');
      }
      else if(i === 0) {
        jsonArray.push('[["' + jsonText + '"],');
      }
      else if(i === (textArray.length - 1)) {
        jsonArray.push('["' + jsonText + '"]]');
      }
      else {
        jsonArray.push('["' + jsonText + '"],');
      }
    }
    json = jsonArray.join('');

    if(jsonNum != 0) {
      localStorage.setItem('startNum', jsonNum); // 番号保存
    }
    else {
      localStorage.setItem('startNum', startNum.value); // 番号保存
    }
    localStorage.setItem('key', json); // ローカルストレージ保存
  }
  else {
    localStorage.clear();
  }

  makedTable.innerHTML = tableText.join('');
  makedExcelTable.innerHTML = excelTable.join('');

  sex.focus();
  other.value = '';
  inquiry.value = '';
  nb.value = '';
} // rendering end

// 修正ラジヲボタン押し下げ
function fix(fixNum) {
  alert('訂正後に修正ボタンを押してね')
  fixText = textArray[fixNum][1].split('	');

  sex.value = fixText[0];
  age.value = fixText[1];
  trigger.value = fixText[2];
  other.value = fixText[3];
  inquiry.value = fixText[4];
  nb.value = fixText[5];
  inputButtonId.value = '修正';
  deleteButtonId.value = 'キャンセル';
  fixGNum = fixNum;
} // fix end

// 最終行削除！
function oneStepBack() {
  const oneStepBackButton = deleteButtonId.value;
  if(oneStepBackButton === '最終行削除') {
    if(oneStepBackNum === 0 && textArray.length <= 1) {
      alert('まだ何もしてません');
    }
    else if(textArray.length > 0) {
      count -= 1;
      textArray.pop();

      rendering();
    }
    else if(textArray.length === 0) {
      alert('取り消しはもうできません');
      sex.focus();
    }
    else {
      console.log('error');
    }
  }
  else if(oneStepBackButton === 'キャンセル') {
    rendering();
    inputButtonId.value = '入力';
    deleteButtonId.value = '最終行削除';
  }
  else {
    console.log('error');
  }

  localStorageConfirmationId.style.display = 'none';
} //oneStepBack end

// タブが入力されてたら
function tabCheck(str) {
  var tab = str.indexOf('\t');
  if(tab != -1){
    str = str.replace(/\t/g, '　');
    if(confirmationNum != 0) {
      return str;
    }
    else {
      if(confirm('タブは全角空白に置き換えます') && confirmationNum === 0) {
        confirmationNum++;
        return str;
      }
      else {
        alert('入力は取り消されました');
        return false;
      }
    }
  }
  return str;
} // tabCheck end

// json取り出し変換
function localStorageConfirmation() {
  try {
    var jsonArray = [];

    var getJsonNum = localStorage.getItem('startNum');
    jsonNum = JSON.parse(getJsonNum);
    if(jsonNum === null) {
      excelNum = startNum.value * 1;
    }
    else {
      excelNum = jsonNum;
    }
    console.log('localStorageConfirmation jsonNum ' + jsonNum);

    var getjson = localStorage.getItem('key');
    var obj = JSON.parse(getjson);
    if(obj === null) {

      alert('ファイルが存在しません');
      // console.log(obj);
    }
    else {
      for(let i = 0; i < obj.length; i++) {
        obj[i][0] = obj[i][0].replace(/'/g, '"'); // 0
        obj[i].push(obj[i][0].replace(/"/g, '')); // 1
        obj[i].push(tableMake(obj, '\t', i, 1)); // 2
        obj[i].push(tableMake(obj, '\t', i, 0)) // 3
        count++;
      }

      textArray = obj;
      rendering();

      punctuationAndStartNum.style.display = 'none';

      oneStepBackNum = 1;
    }
  }
  catch(e) {
    alert(e);
  }

  localStorageConfirmationId.style.display = 'none';
} // localStorageConfirmation end

// テーブル作ります
function tableMake(str, punctuation, countNum ,radio) {
  str = str[countNum][1].split(punctuation);
  for(let i = 0; i < str.length; i++) {
    if(i === 0) {
      if(radio) {
        str[i] = '<tr><td class="radioHead"><label class="radioLabel"><input class="radioButton" type="radio" name="radioButton" onclick="fix(' + countNum + ');"></label></td><td>' + (excelNum + countNum) + '</td><td>' + str[i] + '</td>';
      }
      else {
        str[i] = '<tr><td>' + (excelNum + countNum) + '</td><td>' + str[i] + '</td>';
      }
    }
    else if(i === (str.length - 1)) {
      str[i] = '<td>' + str[i] + '</td></tr>';
    }
    else {
      str[i] = '<td>' + str[i] + '</td>';
    }
  }
  str = str.join('');
  return str;
} // tableMake end

// コピーボタン
function copyTextToClipboard(textVal){
  if(textVal != '') {
    // テキストエリアを用意する
    var copyFrom = document.createElement("textarea");
    // テキストエリアへ値をセット
    copyFrom.textContent = textVal;

    // bodyタグの要素を取得
    var bodyElm = document.getElementsByTagName("body")[0];
    // 子要素にテキストエリアを配置
    bodyElm.appendChild(copyFrom);

    // テキストエリアの値を選択
    copyFrom.select();
    // コピーコマンド発行
    var retVal = document.execCommand('copy');
    // 追加テキストエリアを削除
    bodyElm.removeChild(copyFrom);
    // 処理結果を返却
    return retVal;
  }
  else {
    alert('結果が出力されていません');
  }
} // copyTextToClipboard end

// ExcelTable表示・非表示
function excelTableShowHide() {
  const excelTableShowHideId = document.getElementById('excelTableShowHide');
  const excelTableButtonId = document.getElementById('excelTableButton');
  if(excelTableButtonId.value === 'Excel出力用テーブル表示') {
    excelTableShowHideId.style.display = 'block';
    excelTableButtonId.value = 'Excel出力用テーブル非表示';
  }
  else {
    excelTableShowHideId.style.display = 'none';
    excelTableButtonId.value = 'Excel出力用テーブル表示';
  }
}

// 完全削除
function removeText() {
  localStorage.clear();
  location.reload();
}
```

**※Excelファイルの出力には下記リンクからjsファイルを作ってください**
以上

#最後に

二次元配列仕様でさっぱりしたはずなのに
追加機能でもっと増えてしまった
汚いあれこれですがご勘弁を

コピーはここから拝借しました
[JavaScript でテキストをクリップボードへコピーする方法](https://webllica.com/copy-text-to-clipboard/)

Excelファイルの出力はこちらから
[HTMLのTableをExcelに出力するJavaScript - Qiita](https://qiita.com/tomgoodsun/items/0107e5d778b803935fc0)

JSONフォーマットはここから
[JavaScriptプログラミング講座【JSON について】](https://hakuhin.jp/js/json.html)

addEventListenerはこっち
[【JavaScript入門】addEventListener()によるイベント処理の使い方！](https://www.sejuku.net/blog/57625)