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

#きっかけ
お世話になっている施設でアンケート集計のお仕事がきた
Excelは良いソフトだがプルダウンメニュー選択がクソで大嫌いです
そこでJavascriptを使って集計用のアプリ(?)を作成してみました

#アンドゥ？
個人的には一段階だけでも十分だと思ったのですが意見として多段階アンドゥの実装をと出たので頑張って作ってみました

#コード
スパゲッティすぎて説明もできないので全文のっけます

###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="3" value="1" onchange="startN();">
      </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" size="50" 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" cols="50" placeholder="問い合わせ内容を記入してください。"></textarea>
    </form>
    <!-- 問い合わせ内容 end -->
    <!-- 備考欄 -->
    <form class="tableRow">
      <span class="tableCell textAlignRight vaTop">備考欄：</span>
      <textarea id="nb" class="tableCell" name="name" rows="3" cols="50" placeholder="備考を記入してください。"></textarea>
    </form>
    <!-- 備考欄 end -->
    <!-- 入力・取消・コピー・削除ボタン -->
    <form class="tableRow">
      <span class="tableCell textAlignRight"></span>
      <div class="tableCell">
      <input class="outputButton" type="button" value="入力" onclick="main();">
      <input class="outputButton" type="button" value="取消" onclick="oneStepBack();">
      <span>　</span>
      <input class="outputButton" type="button" value="Copy" onclick="copyTextToClipboard(fullText);">
      <input class="outputButton" type="button" value="全削除" onclick="removeText();">
    </div>
    </form>
    <!-- 入力・取消・コピー・削除ボタン end -->
    <!-- 出力 -->
    <div class="tableRow">
      <span class="tableCell textAlignRight vaTop">確認用テーブル：</span>
      <div id="makedTable" class="tableCell">
        <table>
          <tr>
            <th>番号</th>
            <th>性別</th>
            <th>年代</th>
            <th>きっかけ</th>
            <th>その他理由</th>
            <th>問い合わせ内容</th>
            <th>備考</th>
          </tr>
        </table>
      </div>
    </div>
    <form class="tableRow">
      <span class="tableCell textAlignRight vaTop">出力内容：<br>
      <input id="hide" class="outputButton" type="button" value="表示" onclick="expression();"></span>
      <textarea id="output" class="tableCell" style="display: none;" rows="3" cols="50" placeholder="フォーマットに合わせて出力されます。"></textarea>
    </form>
    <div class="tableRow">
      <span class="tableCell textAlignRight">xlsxファイル：</span>
      <input id="dl-xlsx" class="tableCell outputButton" type="button" value="Download XLSX">
    </div>
    <div class="tableRow" style="display: none;"> <!-- 目隠し -->
      <span class="tableCell textAlignRight vaTop">出力用テーブル：</span>
      <div id="excelTable" class="tableCell">
        <table>
          <tr>
            <th>番号</th>
            <th>性別</th>
            <th>年代</th>
            <th>きっかけ</th>
            <th>その他理由</th>
            <th>問い合わせ内容</th>
            <th>備考</th>
          </tr>
        </table>
      </div>
    </div>
    <!-- 出力 end -->
  </div>
  <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

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

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

span {
  white-space: nowrap;
}

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

table, tr {
  margin: 0;
  padding: 0;
}

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

#startNum {
  text-align: center;
}

.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;
}
```

###js
やっとjsです

```javascript:workSheet.js

const output = document.getElementById('output');
const punctuationAndStartNum = document.getElementById('punctuationAndStartNum');
const makedTable = document.getElementById('makedTable');
const makedExcelTable = document.getElementById('excelTable');
const startNum = document.getElementById('startNum');
var count = 0;
var excelNum = 0;
var initialValue = 0;
var oneStepBackNum = 0;
var fullText = '';
var oneStepBackText = []; // 取消用
var oneStepBackTable = []; // 取消用
var oneStepBackExcel = []; // 取消用
const tag = '<tr><th>番号</th><th>性別</th><th>年代</th><th>きっかけ</th><th>その他理由</th><th>問い合わせ内容</th><th>備考</th></tr>'
var tableText = ['<table>' + tag, '', '</table>'];
var excelTable = ['<table  class="table-to-export" data-sheet-name="testExcel">' + tag, '', '</table>']

function main() {
  const punctuationId = document.getElementById('punctuation');
  const punctuation = punctuationId.value;
  var joined = '';
  var joinedExcel = '';

  const sex = document.getElementById('sex');
  const sexValue = sex.value;

  const age = document.getElementById('age');
  const ageValue = age.value;

  const trigger = document.getElementById('trigger').value;

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

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

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

  if(trigger === 'その他' && otherValue === '') {
    alert('その他の理由を入力してください');
    other.focus();
    return;
  } else if(trigger != 'その他' && 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;
  }

  joined = sexValue + punctuation + ageValue + punctuation + trigger + punctuation + otherValue + punctuation + inquiryValueDQ + punctuation + nbValueDQ;
  joinedExcel = sexValue + punctuation + ageValue + punctuation + trigger + punctuation + otherValue + punctuation + inquiryValue + punctuation + nbValue;

  count += 1;
  oneStepBackExcel.push(excelTable[1]); // 取消用
  oneStepBackTable.push(tableText[1]); // 取り消し用
  tableText[1] = tableMake(joinedExcel, punctuation) + tableText[1];
  excelTable[1] += tableMake(joinedExcel, punctuation);
  makedTable.innerHTML = tableText.join('');
  makedExcelTable.innerHTML = excelTable.join('');

  oneStepBackText.push(fullText); // 取り消し用
  console.log(oneStepBackText);
  fullText += (joined + '\n');

  output.value = fullText;
  const line = countLine(fullText);
  output.style.height = (line * 1.5) + 'em';

  sex.focus();
  other.value = '';
  inquiry.value = '';
  nb.value = '';

  punctuationAndStartNum.style.display = 'none';

  oneStepBackNum = 1;
} // main end

// 最初の番号を取得し-1する
function startN() {
  excelNum = startNum.value - 1;
  initialValue = startNum.value - 1;
}

// 取消！
function oneStepBack() {
  if(fullText != oneStepBackText) {
    count -= 1;
    tableText[1] = oneStepBackTable[count];
    excelTable[1] = oneStepBackExcel[count];
    fullText = oneStepBackText[count];
    oneStepBackTable.pop();
    oneStepBackExcel.pop();
    oneStepBackText.pop();
    makedTable.innerHTML = tableText.join('');
    makedExcelTable.innerHTML = excelTable.join('');
    output.value = fullText;
  } else if(count === initialValue && initialValue === oneStepBackNum) {
    alert('まだ何もしてません');
    sex.focus();
  } else if(fullText === '') {
    alert('取り消しはもうできません');
    sex.focus();
  }
}

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

// テーブル作ります
function tableMake(str, punctuation) {
  str = str.split(punctuation);
  for(var i = 0; i < str.length; i++) {
    if(i === 0) {
      str[i] = '<tr><td>' + (excelNum + count) + '</td><td>' + str[i] + '</td>';
    } else if(i === (str.length - 1)) {
      str[i] = '<td>' + str[i] + '</tr></td>';
    } else {
      str[i] = '<td>' + str[i] + '</td>';
    }
  }
  str = str.join('');
  return str;
}

// コピーボタン
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('結果が出力されていません');
  }
}

// 全削除
function removeText() {
  if(window.confirm('リセットしますか？\n取り消しはできません。')) {
    location.reload();
  }
}

// 出力内容表示・非表示
function expression() {
  const hide = document.getElementById('hide');
  if(hide.value === '表示') {
    output.style.display = 'inline';
    hide.value = '隠す';
  } else {
    output.style.display = 'none';
    hide.value = '表示';
  }
}

// 出力の行数カウント
function countLine(str) {
  num = str.match(/\r\n|\n/g);
  if(num != null){
    line = num.length + 3;
  } else {
    line = 1;
  }
  return line;
}
```

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

#最後に
二次元配列でもっとスマートにできそうな気がしますが今回はここまでとしておきます

コピペ用のテキストもクリップボードに吐き出せますし
Excelファイルも作れます

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

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

レイアウト等のセンスのなさはご勘弁を…

#####追記
アップした後にいじってみたらundoよりも根本的なミスを…
訂正しました