Google Apps Script Google Workspace (G Suite) html Javascript プログラミング

Googleカレンダーへの1行入力アプリをHTML化して使いやすくした

Googleフォームで作った入力画面をHTML化した

2022.08.31追記:月末に投稿すると、イベントの開始日が1ヶ月後ろにズレることがあるバグを修正しました。

以前作ったこのGASのWebアプリは結構役立っています。特にモバイルで予定を素早く入力できるのがいいです。

ただ、GoogleフォームをUIに利用しているので、ちょっと使い勝手が悪いのです。

そこで、UIをHTML化して使いやすくしました。

HTML化により、専用のシンプルな画面と大きな文字で入力しやすくなりました。

入力中にはリアルタイムにデータをチェックして、どのように解釈しているかを下のプレビューエリアに表示します。

また,カレンダー毎に投稿ボタンを設けることで、1アクションで投稿とカレンダー選択ができるようになりました。

投稿ボタンは有効な入力データが揃うと有効になります。

こうした改良によって格段に使いやすくなりました。

プロジェクトの時間帯を日本時間に設定

このアプリはスプレッドシートに紐付いたContainer Bound Scriptとして作ります。紐付けたスプレッドシートに投稿データを記録してログ代わりとしています。

新規にGoogleスプレッドシートを作成し、スプレッドシートの「拡張機能」メニューから「Apps Script」を選んでスクリプトエディターに入ります。

まず、投稿時間が海外時間と解釈されるのを防ぐために、GASプロジェクトのタイムゾーンをAsia/Tokyoに設定しました。

まず、プロジェクトの設定で"「appsscript.json」マニフェスト ファイルをエディタで表示する"をオンにします。

コードタブにappsscript.jsonが表示されるので、"timeZone": "Asia/Tokyo",と設定します。

{
  "timeZone": "Asia/Tokyo",
  "dependencies": {},
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8",
  "webapp": {
    "executeAs": "USER_DEPLOYING",
    "access": "MYSELF"
  }
}

GAS側のコード(コード.js)

GASのコードでは、doGet()関数の中でHtmlService.createTemplateFromFile()を使ってindex.htmlを返します。

ほかにはカレンダーを登録する時にHTMLから呼ばれるpostEvent()関数とその引数からカレンダー登録用データのオブジェクトを作るparseEventDataFromFormData()関数があります。

基本的にはGoogleフォーム版からの流用です。

const sheet = SpreadsheetApp.getActiveSheet();

function doGet() {
  // return HtmlService.createHtmlOutputFromFile('index');

  const htmlOutput = HtmlService.createTemplateFromFile('index').evaluate();
  htmlOutput.setTitle('1行で予定登録');
  return htmlOutput;
}


function postEvent(calendarId, formData) {
  sheet.appendRow([new Date(), formData]);
  const parsedEventData = parseEventDataFromFormData(formData);
  let calendar = CalendarApp.getCalendarById(calendarId);
  const title = parsedEventData.title || "項目が空でした";
  let startTime = new Date(parsedEventData.time);
  if (parsedEventData.isAllDay) {
    calendar.createAllDayEvent(title, startTime);
  } else {
    let endTime = new Date(startTime.getTime());
    endTime.setHours(startTime.getHours() + 1);//とりあえず開始時間の1時間後を終了時間にする
    calendar.createEvent(title, startTime, endTime);
  }
}

function parseEventDataFromFormData(formData) {
  const data = formData.split(/[\s\.:]/);
  // data[0]:月、data[1]:日、(data[2]:時、data[3]:分)、data[.] or data[4]:タイトル
  let isAllDay = false;
  let title = "";
  let eventTime = new Date();
  if (eventTime.getMonth() > (data[0] - 1)) { //今月より前であれば来年にする
    eventTime.setFullYear(eventTime.getFullYear() + 1);
  } else if (eventTime.getMonth() == (data[0] - 1) && eventTime.getDate() > data[1]) {
    eventTime.setFullYear(eventTime.getFullYear() + 1);//今月でも日にちが前なら来年にする
  }
  eventTime.setDate(1);//いったん日付を1にする。月を設定する時のオーバーフロー防止
  eventTime.setMonth(data[0] - 1);//Calenderの月パラメーターは0始まりなので1を引く
  eventTime.setDate(data[1]);
  if (data.length > 4) {
    eventTime.setHours(data[2]);
    eventTime.setMinutes(data[3]);
    title = data[4];
  } else { //時間が省略されていたら終日イベントにする
    isAllDay = true;
    title = data[2];
  }
  return { time: eventTime, title: title, isAllDay: isAllDay };
}

HTML+Javascriptのコード

HTML内にscriptタグでJavascriptを記述しています。

index.htmlは、スクリプトエディタのファイル追加で"HTML"を選択して作ります。

Googleフォーム版には無い機能として、keyupイベントの度に入力データの簡易チェックを行っています。

ちゃんと入力されていると、入力欄の下に解釈した結果が表示されます。間違っていると表示されません。

チェックは完璧ではありませんが、無いよりはずっとマシです。

誤投稿を防ぐために、必要な情報が揃ったところで投稿ボタンをアクティブにしています。また、ボタンをカレンダー毎に分けて、カレンダー選択と投稿の操作を一体化しています。

GoogleカレンダーのAPIは割とおおらかというか、日付とかが間違っていてもエラーになりません。例えば閏年でもないのに2/29とかを指定しても、3/1に登録されます。また、数値でない日付データを渡してみたら、たしか1970年1月1日とかになりました。

なので、投稿後にGoogleカレンダーアプリで登録できたかどうかは確認したくなります。あー、このアプリに投稿後の確認機能も付ければいいのか(つけるとは言ってない)。

一応、スプレッドシートにも記録するのでそっちで確認する手もあります。

また、3つ以上のカレンダーをお使いの型はHTMLも編集してボタンを追加するなどしてください。

CSSはBulmaというフレームワークを初めて使いました。シンプルでいいですね。

https://bulma.io

以下のコードはこのままでは動きません。お使いになる際には自分のカレンダーIDを設定してください。

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>1行で予定登録</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.min.css">
</head>

<body>
  <section class="section">
    <div class="container">
      <h1 class="title">
        予定を入力
      </h1>
      <p class="subtitle">
        例:3.27.17.00.待ち合わせ<br>
      </p>
      <div class="field">
        <div class="control">
          <input type="text" id="input_field" class="input is-primary is-large" placeholder="月.日.時.分.タイトル">
        </div>
      </div>
      <div class="field">

        <input type="text" class="input is-static is-large readonly" id="preview" placeholder="変換結果">
      </div>

      <div class="field is-grouped">
        <div class="control">
          <button disabled class="button is-link is-medium" id="primary">仕事</button>
        </div>
        <div class="control">
          <button  disabled class="button is-link is-danger is-medium" id="event">遊び</button>
        </div>
        <div class="control">
          <button class="button is-link is-light is-medium ml-6" id="clear">Clear</button>
        </div>

      </div>
    </div>
  </section>

  <script type="text/javascript">
    const calendarId_primary = "primary";
    const calendarId_event = "--------------- 自分のカレンダーのID -----------------";

    const input_field = document.getElementById("input_field");
    const preview = document.getElementById("preview");

    const primary_button = document.getElementById("primary");
    const event_button = document.getElementById("event");

    const clear_button = document.getElementById("clear");


    input_field.addEventListener("keyup", function (e) {
      if (previewEventData(this.value)) {
        console.log("true");
        setPostButtonStatus(true);
      } else {
        console.log("false");
        setPostButtonStatus(false);
      }
    });

    clear_button.addEventListener("click", e => {
      input_field.value = "";
      preview.value = "";
    });

    primary_button.addEventListener("click", function(e) {
      google.script.run
      .withSuccessHandler(success)
      .withFailureHandler(failture)
      .postEvent(calendarId_primary,input_field.value);
    });

    event_button.addEventListener("click", function(e) {
      google.script.run
      .withSuccessHandler(success)
      .withFailureHandler(failture)
      .postEvent(calendarId_event,input_field.value);
      console.log(e);
    });

    function success(e){
        preview.value="OK! " + preview.value;
    }

    function failture(e){
      preview.value="Failed to Write. " + preview.value;
      console.log(e);
    }

    function setPostButtonStatus(flag) {
      primary_button.disabled = !flag;
      event_button.disabled = !flag;
    }

    function previewEventData(d) {
      const data = d.split(/[\s\.:]/);
      let previewText = "";
      const MONTH_DAY = [0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];//データチェック用の各月の日数上限。うるう年はノーチェック
      let checkData = false;
      if (data[0] && (data[0] > 0 && data[0] <= 12)) {
        previewText = data[0] + "月"; //月
      }
      if (data[1] && (data[1] > 0 && data[1] <= MONTH_DAY[data[0]])) {
        previewText += data[1] + "日";//日
      }

      if (data.length > 3) {
        if (data[2] && (data[2] > -1 && data[2] <= 24)) {
          previewText += data[2] + "時";//時
        }
        if (data[3] && (data[3] > -1 && data[3] <= 59)) {
          previewText += data[3] + "分";//分
        }
        if (data[4]) {
          previewText += ":" + data[4];//タイトル
          checkData = true;
        }
      } else {
        if (data[2] && isNaN(data[2])) {
          previewText += ":" + data[2];//タイトル
          checkData = true;
        }
      }
      preview.value = previewText;
      return checkData;
    }

  </script>

</body>

</html>

必要なコードを設定し終わったら、Webアプリとしてデプロイして、権限を設定すれば使えるはずです。

やっぱりUIは大事

まだそれほど回数は使っていませんが、やはり見やすい画面と最小減の操作で使えるのはいいですね。

-Google Apps Script, Google Workspace (G Suite), html, Javascript, プログラミング