20210322のJavaScriptに関する記事は15件です。

XMLHttpRequestについて

XMLHttpRequestとはデータをクライアントとサーバー間で通信できるオブジェクトのことです。

よくAjaxで使用されます。

名前にXMLとついていますがXMLだけでなくtextやjsonといったデータも扱えます。

XMLHttpRequestは同期・非同期の両方で通信することができますが、基本的には非同期で使用します。

ここでは最低限のXMLHttpRequestの使用方法を書いていく。

使用方法

XMLHttpRequestのインスタンスを作成。

const xhr = new XMLHttpRequest();

初期化

xhr.open(method, URL);

openと書いてありますがここでは通信をしません。

  • methodの部分はGETやPOSTなどHTTPメソッドを入れます。
  • URLの部分は通信したいURLをいれます。

初期化したものを送る

xhr.send([body]);

sendで初めて通信が行われる。

GET通信のときはbodyは必要ありません。POST通信の時に使用します。

XMLHttpRequestはいくつかイベントを持っています。

  • loadstart - リクエスト送信時
  • progress - データを送受信している途中
  • timeout - リクエストがタイムアウトした時
  • abort - リクエストをキャンセルした時
  • load - リクエスト送信時
  • error - リクエストエラー
  • loadend - 正常・異常にかかわらずリクエストが完了した時

このイベントを使用してサーバーからの応答を受け取ります。

xhr.addEventListener("load", () => {
    if (xhr.status === 200) { // 
        console.log(xhr.responseText);
    }
});

// エラーの場合はここが呼ばれる。
xhr.addEventListener("error", () => {
    console.log("error");
});

リクエストを送信しstatusが200と問題なければテキストとして出力する。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

フォームの送信ボタンの種類によってエンターキー押下時のonSubmitの挙動が変わるか軽く調べてみた

<form /> の送信ボタンで <input type="submit" value="送信" /><button>送信</button> でなんか動きが違うのかな?というのを軽く調べてみました。
結論から言うと多分送信ボタンとしての挙動自体に差はなさそう
少なくとも <input type="submit" value="送信" /><button>送信</button> の二つをおいてエンターキーを押したときのイベント発火順序は両方とも同じでした
codepen.io にサンプルコードをおいたので興味のある人はどうぞ

なんとなく、お前らのsubmitはもう古い!みたいなネタ風に書こうかと思いましたが、今回は興が乗らなかったのでやめました

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

シンプルで使いやすいURLエンコード・デコードツールを作ってみた

はじめに

こんにちは、システム開発チーム「presto」です。
開発業務(プログラミング中)に「こんなツール、あんなツールが身近なところにあったらいいなぁ」と思ったことはありませんか?

ここではそんなツールをVuetifyを使って作ってみたので紹介させてください。

URLエンコード・デコードツール

今回紹介させていただくのは、URLエンコード・デコードツールです。

画面は以下の通りです。

URLエンコード・デコードツール

使い方

使い方は以下の通りです。
1. 変換前のURL文字列を入力する
2. 変換方法を選択する
3. 結果確認

1. 変換前のURL文字列を入力する

変換前のURL文字列を入力する

例として、以下を入力します。

https://あいうえお.com/

変換前のURL文字列を入力する

2. 変換方法を選択する

変換方法を選択する

変換方法は以下の4通りがあります。
エンコードもしくはデコードのそれぞれを選んでください。

変換方法 概要
EncodeURI URI (Uniform Resource Identifier; 統一資源識別子) をエンコードし、各文字のインスタンスをそれぞれ UTF-8 符号の文字を表す 1 個から 4 個のエスケープシーケンスに置き換えます (サロゲート文字のペアのみ 4 個のエスケープシーケンスになります)。
参考 encodeURI()|MDN web Docs

以下、エンコードされない文字列
A-Z a-z 0-9 ; , / ? : @ & = + $ - _ . ! ~ * ' ( ) #
EncodeURIComponent URI (Uniform Resource Identifier) 構成要素を特定の文字を UTF-8 文字エンコーディングで表された 1 個から 4 個のエスケープシーケンスに置き換えることでエンコードします (サロゲートペアで構成される文字のみ 4 個のエスケープシーケンスになります)。
参考 encodeURIComponent()|MDN web Docs

以下、エンコードされない文字列
A-Z a-z 0-9 - _ . ! ~ * ' ( )
DecodeURI encodeURI() 関数あるいは同様のルーチンによって事前に作成された URI (Uniform Resource Identifier; 統一資源識別子) をデコードします。
参考 decodeURI()|MDN web Docs
DecodeURIComponent encodeURIComponent() 関数あるいは同様のルーチンによって事前に作成された URI (Uniform Resource Identifier; 統一資源識別子) の構成要素をデコードします。
参考 encodeURIComponent()|MDN web Docs

3. 結果確認

上記例でEncodeURLした場合、以下のような値が表示されます。

https://%E3%81%82%E3%81%84%E3%81%86%E3%81%88%E3%81%8A.com/

エンコード結果

エンコード済みの文字列をデコードする場合、以下のような値が表示されます。

https://あいうえお.com/

デコード結果

まとめ

今回は、URLエンコード・デコードツールの紹介をさせていただきました。

JSONフォーマットツールの紹介もしていますので、良ければ合わせてご覧ください。

今後もよろしくお願いします。
ありがとうございましたー!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

総務がGASで請求書業務を半自動化してみた。

◎登場人物紹介◎

総務
現在22歳。入社1年3ヶ月。フィグニー唯一の総務。
月末と月初は請求業務と給与計算に追われている。心配性な性格のため業務中は頭と胃が痛いことが多い。好きな食べ物は和菓子の練りきり。


現在21歳。
運良くフィグニーに入社して早1年。
朝から晩までコードを書いている。今はインフラの沼に飲み込まれてる。好きな作業場所はソファ。


総務:請求書業務自動化したい!!!!!


さて... 第1回目の本日は、「総務がGASで請求書業務を半自動化してみた。」です!!

弊社は開発会社なのですが、バックオフィス業務はIT化が遅れている部分が多々あります。その最たるものが請求書業務です!
「SalesForce」「らくらく明細」をはじめとした便利なツールは有料で社長の決裁が下りなかったので(ケチ)、自分で作ることにしました。

総務一人では作れないのでサポートエンジニアがついて教えてくれます!
2年後にはチームの柱になる 郷将輝くん(以下「」という)です!
minami.png
総務:よろしくおねがいします!!
masaki.png
:よろしくお願いします。

1.目標

下記のように手動で行っていたことをGASを書いて自動化させていくのが目標です。

案件リスト(スプレッドシート)ステータスが[作成待ち]になっている案件の数を確認
②案件の数だけ雛形請求書(スプレッドシート)を手動コピー
③案件ごとに案件リスト内の[請求日][入金予定日][顧客名]を請求書の該当する箇所にコピペ(それ以外は発注書等を見て入力するので今回は自動化しない)
④請求書ファイル名を手動で変更して請求書管理フォルダに移動
eb5677dc-4734-4a15-9577-747910a0e569.png
64d42f30-f6bd-4262-b66a-c34a84b82f70.png

2.事前準備

下記のように2つのテスト用テンプレート1つのテスト用フォルダを作成しました。

・案件リスト(スプレッドシート)
・雛形請求書(スプレッドシート)
・完成したスプレッドシートが入るフォルダ


masaki.png
:事前準備は終わったので、GASのコードを書きましょう。
minami.png
総務:いよいよここから未知の領域ですね!!

3.手順

流石に総務は素人のためいきなり一人ではかけません。
そのため以下の手順で取り組んでいくことにしました。

①ProgateでJavaScriptを学ぶ。
②とりあえず調査。(検索力が試される。)
③柱に教えてもらう。

masaki.png
:それでは書いてみましょう。

4.GASでスクリプトを書く

①案件リスト(スプレッドシート)を開く
②上部メニュー「ツール>スクリプト エディタ」を選択
③コードを入力

以下が今回書いたコードです。

// Spreadsheetが開かれた時に自動的に実行
function onOpen() {

// 現在開いている、スプレッドシートを取得
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();

// メニュー項目を定義
var entries = [
  {name : "請求書作成",functionName : "create"}];

// 「書類作成」という名前でメニューに追加
spreadsheet.addMenu("書類作成", entries);
}

function create(){

 // 現在開いている、スプレッドシートのシートを取得
 var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
 var sh = spreadsheet.getActiveSheet();

 //ステータス列を取得
 var range_list = sh.getRange(2, 12, sh.getLastRow()-1).getValues();

 //2次元配列を1次元配列にする
 var editarray = Array.prototype.concat.apply([],range_list);

 //ステータス列のデータの値を取得
 for(var i = 0; i < editarray.length; i++){

  if(editarray[i] === "作成待ち"){

    //作成待ちの行を取得
    var cell = sh.getRange("L"+(i+2))
    var row = cell.getRow();

    //作成待ちの行の特定のセル(項目)の値を取得
    var rof = "A" + row + ":" + "F" + row;  
    var cell2 = sh.getRange(rof).getValues();

    //2次元配列を1次元配列にする
    var ss = cell2[0];

    // 請求書Noの表記を変更して取得
    var a = ss[0];
    invoice = Utilities.formatDate(a,"JST", "yyyyMMdd");
    var invoiceNo = invoice + (i+1)
    // 請求日の表記を変更して取得
    billingdate = Utilities.formatDate(a,"JST", "yyyy/MM/dd");
    // 入金期限の表記を変更して取得
    var b = ss[3];
    depositdate = Utilities.formatDate(b,"JST", "yyyy/MM/dd");
    // 会社名の値を取得
    var company = ss[5];

    //雛形の請求書(スプレッドシート)
    var templateFile = DriveApp.getFileById("xxxxxxxxxxxxx");
    // 完成したスプレッドシートが入るフォルダ
    var OutputFolder = DriveApp.getFolderById('xxxxxxxxxxxxx');
    // 出力ファイル名
    var CopiedFile = templateFile.makeCopy( "請求書_"+ invoice + "_"+ company +"_Fignny", OutputFolder );
    // コピーしたシートのID取得
    var CopiedFileId = CopiedFile.getId(); 
    //値を貼り付けする請求書
    var ss_copyTo = SpreadsheetApp.openById(CopiedFileId);

    //貼り付けするセル指定
    ss_copyTo.getRange("H10:Q10").setValue(invoiceNo);
    ss_copyTo.getRange("H11:Q11").setValue(billingdate);
    ss_copyTo.getRange("H12:Q12").setValue(depositdate);
    ss_copyTo.getRange("C6:O7").setValue(company);

    //[作成待ち]を[作成済み]に変更
    var completerow = "L" + (i+2)
    sh.getRange(completerow).setValue("作成済み");
}
}
}

今回は総務のチャレンジ企画なのでブロックごとに見ていきます。
ここから長くなりますがお付き合い下さい。(笑)

案件リスト(スプレッドシート)の上部に「書類作成」メニューを追加

minami.png
総務:一番最初に行うのはこれですね。調査してみます!!

:この処理はGAS側で書き方が定義されているものなので調べてみましょう。

総務:( 調査中・・・ )

総務:...うわああ!できた!!(すごい!感動!感激!)
   ※素人なので一喜一憂します。

// Spreadsheetが開かれた時に自動的に実行
function onOpen() {

// 現在開いている、スプレッドシートを取得
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();

// どのようなメニュー項目の名前か、そのメニュー項目を押下した時どのような処理をするかを決る
var entries = [
  {name : "請求書作成",functionName : "create"}];

// 「書類作成」という名前でメニューに追加
spreadsheet.addMenu("書類作成", entries);
}

72de3622-083e-4d59-a4a6-6d065d8a9259.png
masaki.png
:[請求書作成]が押下されたときに行う処理をfunctionNameというのに指定しました。
  今回はcreateとしましたが任意の名前でOKです。次はcreateという関数の処理を書きましょう。

総務:なるほど!!

:まずはやりたいことを頭の中で整理しましょう。

minami.png
総務:はい。案件リストのステータスが[作成待ち]の案件だけ、請求書を作成したいから・・・。742a850d-eca9-4952-9527-ea80b3d5e97b.png
総務: L列のステータスの値を取得したいですね。

ステータス列の値を取得

minami.png
総務:( 調査中・・・ )

総務:できました!!

 //ステータス列を取得
 var range = sh.getRange("L2:L9").getValues();
 console.log(range);

masaki.png
:L2:L9というのを固定してしまいますと、例えば次の月は案件が一個増えた場合L2:L10となってしまいますが、毎回変えるのですか?

総務:んー、確かに...。ちょっと調査してみます!!
minami.png
総務:記事にこんな内容が載ってました!

行番号は、いつも2行で一定というわけではなく可変です。
そこで、getLastRowメソッドを使って、シート上にデータのある最終行の行番号を取得するようにします。

:そうですね。これで書いてみましょう。

総務:できたかな?

 //ステータス列を取得
 var range = sh.getRange(2, 12, sh.getLastRow()).getValues();
  console.log(range);
ログ
[ [ '作成待ち' ],
  [ '作成待ち' ],
  [ '作成待ち' ],
  [ '作業中' ],
  [ '作成済み' ],
  [ '作業中' ],
  [ '作業中' ],
  [ '作業中' ],
  [ '' ] ]

minami.png
総務:おー!これが配列か!!...あれ??何故か最後に空白があります。

:空白配列ですかね。getLastRowをログに出力してみましょう。

ログ 9

総務:9...??
masaki.png
:先程参考にした記事を最後までよく見るとこのような記載があります。

今回の例では、データのある最終行数は3です。
ただ、見出し行を除きたいので、マイナス1をして、結果として2が行数の指定となるべき、ということです。

総務:ハッ!!なるほど!!案件リストにも見出しがある!

総務:できた!!

 //ステータス列を取得
 var range = sh.getRange(2, 12, sh.getLastRow()-1).getValues();
  console.log(range);
ログ
[ [ '作成待ち' ],
  [ '作成待ち' ],
  [ '作成待ち' ],
  [ '作業中' ],
  [ '作成済み' ],
  [ '作業中' ],
  [ '作業中' ],
  [ '作業中' ] ]

minami.png
総務:...箱の中に箱が複数ある状態だ。これが二次元配列か〜。

:二次元配列より一次元配列の方がシンプルに値を取り出せるから二次元配列を一次元配列にしてみましょう。

総務:(調査中・・・。)

総務:できました!!
※配列だとログで理解したので変数のrangeはrange_listに変更しました。

 //2次元配列を1次元配列にする
 var editarray = Array.prototype.concat.apply([],range_list);
  console.log(editarray);
ログ [ '作成待ち', '作成待ち', '作成待ち', '作業中', '作成済み', '作業中', '作業中', '作業中' ]

masaki.png
:OKですね。ここまでは列のデータを配列として取得しただけなので、ここから値を取得していきましょう。

:月ごとで案件数は違うので、最後の案件までの値を取得してそこまでfor文でループを回せばいいと思います。

:先程一次元配列にしたものを変数editarray(↓)にしましたね。それを使うんですよ!

ログ [ '作成待ち', '作成待ち', '作成待ち', '作業中', '作成済み', '作業中', '作業中', '作業中' ]

minami.png
総務:なるほど!配列を見るとわかりやすいな〜。配列ではインデックス番号は0からスタートだから.....

総務:苦戦したけど、できた!!全然理解できなかったfor文が実用できたー!(涙)

 //ステータス列のデータの値を取得
 for(var i = 0; i < editarray.length; i++){
  console.log(editarray[i]);
ログ
   作成待ち
   作成待ち
   作成待ち
   作業中
   作成済み
   作業中
   作業中
   作業中

minami.png
総務:次は請求書内にコピペする案件リスト内の特定の項目の値が欲しいなぁ。
あくまでも請求書を作成するのはステータスが[作成待ち]のものだけなので...(赤枠)
07079011-66af-430b-8f6a-106e5592f2d2.png
masaki.png
:if文でステータスが[作成待ち]の時の行を取得してから特定のセル(項目)の値を取得する処理をかけばできるのでは?

総務:(???)そういうことか....!理解するのに時間かかりました(笑)

ステータスが[作成待ち]の行を取得

masaki.png
:配列の中の値をひとつずつ見ていき、作成待ちのものを◯◯するという処理をかきたいのでfor文の中にif文をかきましょう。
minami.png
総務:なるほどこれが条件分岐ってやつですか(汗)

総務:配列editarrayではインデックス番号は0からスタート、スプレッドシートの行数に合わせるには変数 i に+2(見出しがあるため)をすれば[作成待ち]のセルの範囲を取得することができるから、行数も取得できるのか。

総務:なんとかできました...。(※最初は+2をせずに違う行数を取得していました...。)

 //ステータス列のデータの値を取得
 for(var i = 0; i < editarray.length; i++){
  console.log(editarray[i]);

  if(editarray[i] === "作成待ち"){

    //作成待ちの行を取得
    var cell = sh.getRange("L"+(i+2))
    var row = cell.getRow();
    console.log(row);
ログ
 作成待ち
  2
 作成待ち
 3
 作成待ち
 4
 作業中
 作成済み
 作業中
 作業中
 作業中

作成待ちの行の特定のセル(項目)の値を取得

minami.png
総務:欲しいのは、[請求日][入金予定日][顧客名]なのでA列からF列まで。
    例えば、見出しを除いて一行目が[作成待ち]だったら、A2:F2ということになるのか...
でも行数は可変するので(二行目だったらA3:F3.....)どうすればいいんだろう。
masaki.png
:A列とF列という文字と、先程、作成待ちの行を取得した変数rowを連結させれば….

総務:そうか。それでできるのか。(Progateでやった記憶がある模様)

総務:すごい!取得できてる!!

    //作成待ちの行の特定のセル(項目)の値を取得
    var rof = "A" + row + ":" + "F" + row;  
    var cell2 = sh.getRange(rof).getValues();
    console.log(cell2);
ログ
作成待ち
 [ [ Tue Mar 30 2021 11:00:00 GMT-0400 (アメリカ東部夏時間),
    '未',
    '未',
    Thu Apr 29 2021 11:00:00 GMT-0400 (アメリカ東部夏時間),
    'あいうえ開発',
    'あいうえ株式会社' ] ]
作成待ち
 [ [ Tue Mar 30 2021 11:00:00 GMT-0400 (アメリカ東部夏時間),
    '未',
    '未',
    Thu Apr 29 2021 11:00:00 GMT-0400 (アメリカ東部夏時間),
    'かきこけ開発',
    '株式会社かきこけ' ] ]
作成待ち
 [ [ Tue Mar 30 2021 11:00:00 GMT-0400 (アメリカ東部夏時間),
    '未',
    '未',
    Thu Apr 29 2021 11:00:00 GMT-0400 (アメリカ東部夏時間),
    'さしすせ開発',
    'さしすせ株式会社' ] ]
作業中
作成済み
作業中
作業中

masaki.png
:ここまできたら後は簡単ですね。時間の表記の変更の仕方は調べればすぐにでてくるのでやってみてください!
minami.png
総務:はい!まずは二次元配列を一次元配列にかえて〜、時間の表記を適切なものに変更したものを変数にいれていきます〜。

:あっそうだ。二次元配列を一次元配列にする方法は多数あるのですが違う方法を教えますね。

総務:はい!お願いします!
masaki.png
:この図を見て下さい。箱の中に複数箱が入っている状態ではなく、箱の中に一つの箱しか入っていない場合は、配列名[0]で一次元配列でだすことができます。
e0b801a4-e984-4dd9-a0e2-d85ef7b2d3e9.jpeg
総務:わかりやすいです!その方法もあるのか!

総務:お!できました!

:いいですね〜。

    //2次元配列を1次元配列にする
    var ss = cell2[0];
    console.log(ss);
ログ
作成待ち
 [ [ Tue Mar 30 2021 11:00:00 GMT-0400 (アメリカ東部夏時間),
    '未',
    '未',
    Thu Apr 29 2021 11:00:00 GMT-0400 (アメリカ東部夏時間),
    'あいうえ開発',
    'あいうえ株式会社' ] ]
[ Tue Mar 30 2021 11:00:00 GMT-0400 (アメリカ東部夏時間),
  '未',
  '未',
  Thu Apr 29 2021 11:00:00 GMT-0400 (アメリカ東部夏時間),
  'あいうえ開発',
  'あいうえ株式会社' ]
作成待ち
[ [ Tue Mar 30 2021 11:00:00 GMT-0400 (アメリカ東部夏時間),
    '未',
    '未',
    Thu Apr 29 2021 11:00:00 GMT-0400 (アメリカ東部夏時間),
    'かきこけ開発',
    '株式会社かきこけ' ] ]
[ Tue Mar 30 2021 11:00:00 GMT-0400 (アメリカ東部夏時間),
  '未',
  '未',
  Thu Apr 29 2021 11:00:00 GMT-0400 (アメリカ東部夏時間),
  'かきこけ開発',
  '株式会社かきこけ' ]

※表記については、以下のようにする。
(請求書Noは請求日に案件リストの行数を付け加えたものとしている)
65e878fa-83fd-499a-8de0-9f7242b6cf35.png
minami.png
総務:ということで、表記変更と値の取得ができました!!

    // 請求書Noの表記を変更して取得
    var a = ss[0];
    invoice = Utilities.formatDate(a,"JST", "yyyyMMdd");
    var invoiceNo = invoice + (i+1)
    console.log(invoiceNo);
    // 請求日の表記を変更して取得
    billingdate = Utilities.formatDate(a,"JST", "yyyy/MM/dd");
    console.log(billingdate);
    // 入金期限の表記を変更して取得
    var b = ss[3];
    depositdate = Utilities.formatDate(b,"JST", "yyyy/MM/dd");
    console.log(depositdate);
    // 会社名の値を取得
    var company = ss[5];
    console.log(company);
ログ
[ Tue Mar 30 2021 11:00:00 GMT-0400 (アメリカ東部夏時間),
  '未',
  '未',
  Thu Apr 29 2021 11:00:00 GMT-0400 (アメリカ東部夏時間),
  'あいうえ開発',
  'あいうえ株式会社' ]
202103311
2021/03/31
2021/04/30
あいうえ株式会社

[作成待ち]の案件数の請求書のスプレッドシートを作る

minami.png
総務:請求書に貼り付けしたい値が取得できたので、今度は貼り付けするシートを作りたい!
masaki.png
:ここからの処理はGAS側で書き方が定義されているものなので調べればでてきますよ。
minami.png
総務:わかりました!

総務:(調査中・・・)

総務:まずは事前に準備した雛形請求書(スプレッドシート)と完成したスプレッドシートが入るフォルダのIDも取得しないと! ほほー、xxxxx の部分にシートとフォルダのIDを入れれば取得できるのか〜!

 // 雛形請求書(スプレッドシート)
 var templateFile = DriveApp.getFileById("xxxxxxx");
 // 完成したスプレッドシートが入るフォルダ
 var OutputFolder = DriveApp.getFolderById('xxxxxxx');

総務:そしたらファイル名も決めておこう。 請求書のファイル名は[請求書xxxx年xx月xx日顧客会社名_自社名]にしたいのでうまく文字列と変数を連結させればできますね。

// 出力ファイル名
var CopiedFile = templateFile.makeCopy( "請求書_"+ invoice + "_"+ company +"_Fignny", OutputFolder );

総務:あれ??雛形のスプレッドシートのIDしか取得してないな。これだと雛形に案件リストの値が貼り付けされてしまうのか。コピーしたシートのID取得する方法あるのかな〜。また調査だ。。。

総務:すぐでてきました(笑)これで案件数のスプレッドシートができるぞ〜!

// コピーしたシートのID取得
var CopiedFileId = CopiedFile.getId(); 
//値を貼り付けする請求書
var ss_copyTo = SpreadsheetApp.openById(CopiedFile);

masaki.png
:いいですね。調べ方も慣れてきたんですかね?(笑)

取得した値を貼り付けする

minami.png
総務:ここはもう簡単ですな!

//貼り付けするセル指定
ss_copyTo.getRange("H10:Q10").setValue(invoiceNo);
ss_copyTo.getRange("H11:Q11").setValue(billingdate);
ss_copyTo.getRange("H12:Q12").setValue(depositdate);
ss_copyTo.getRange("C6:O7").setValue(company);

:もう少しですね。

総務:やっとです(汗)

ステータスを[作成待ち]から[作成済み]に変更

minami.png
総務:よしゃ〜!!!できた〜!!!

var completerow = "L" + (i+2)
sh.getRange(completerow).setValue("作成済み"); 

masaki.png
:お疲れ様です!!
minami.png
総務:お疲れ様です!!ありがとうございます!!!

5.感想

今回は初めてプログラムを書いて動くものを作ってみました!

素人なので、最初は ”簡単そうだなぁ” と思っていましたが、ProgateでJavaScriptを少し学んだだけでは、なかなか難しいものでした。
経験がないため「これくらい簡単でしょ」というクライアント様が多いのも理解できたし、そういうときにエンジニアが必ず顔をしかめる理由もわかりました。(笑)
弊社の代表はエンジニア社長なので、両者の気持ちがわかった上で仕事を請けてくれるので良かったと思いました。

疑問や不明点はサポートエンジニアの柱に聞くとすぐに正解が分かるため、自分の意固地な性格もあり、”自力で調べて理解してやる。”と3-4時間調査してた時もありました。
その時に柱が「その気持ち分かります。悔しいけど、本当の仕事なら納期というものがあるから聞かないといけないんですよね。でも、悔しい気持ちを成長に変えれる。」と仰っていて、違う観点からもエンジニアという仕事はすごいと改めて思いました。

”請求書を自動作成するツールなんていくらでもあるじゃないか”と思う方もいるかもしれません。しかも、もっと便利なやつ。
ですが自分で作ってみることで、作業効率化は素晴らしいと感じ意欲的になった事や、総務として支えている専門職の方たちの凄さが実感できました!!

今度は何をしようか考えるのが楽しみです!

6.参考にさせていただいたサイト

【超初心者向け!】GASの二次元配列をやさしく図入り解説! - Yuki's bnb blog

Google Apps Scriptでスプレッドシートの列データを配列として取得する方法

【GAS】for文をわかりやすく理解する方法【めがね式】 | 100メガ

二次元配列を一次元配列に変換する方法〜GoogleAppsScript〜 | GAS開発記録

Google Apps Script(GAS)の日付を文字列に変換する方法(formatDateメソッド) | AutoWorker〜Google Apps Script(GAS)とSikuliで始める業務改善入門

【Google Apps Script】移動・複製・リネーム・ID取得・読み書きする方法 [Spreadsheets] | CGメソッド

JavaScript | 配列の要素の値の取得と新しい値の代入

二次元配列とは|「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典

for文とは|「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典

JavaScriptの「Array.prototypeメソッド」の全30メソッドを解説【ES2016版】 | maesblog

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[JS]Tab Menuについて

はじめに

こちらも順序整理の為に記していきます。

実装

まずは形を作っていきます。

index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Tab Menu</title>
  <link rel="stylesheet" href="css/styles.css">
</head>
<body>
  <div class="container">
    <ul class="menu">
      <li><a href="#" class="active">サイトの概要</a></li>
      <li><a href="#">サービス内容</a></li>
      <li><a href="#">お問い合わせ</a></li>
    </ul>

    <section class="content active">
      サイトの概要。サイトの概要。サイトの概要。サイトの概要。
      サイトの概要。サイトの概要。サイトの概要。サイトの概要。
    </section>

    <section class="content">
      サービス内容。サービス内容。サービス内容。サービス内容。
      サービス内容。サービス内容。サービス内容。サービス内容。
    </section>
    <section class="content">
      お問い合わせ。お問い合わせ。お問い合わせ。お問い合わせ。
      お問い合わせ。お問い合わせ。お問い合わせ。お問い合わせ。
    </section>
  </div>


  <script src="js/main.js"></script>
</body>
</html>

css/styles.css
body {
  font-size: 14px;
}

.container {
  margin: 30px auto;
  width: 500px;
}

.menu {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
}

.menu li a {
  display: inline-block;
  width: 100px;
  text-align: center;
  padding: 8px 0;
  color: #333;
  text-decoration: none;
  border-radius: 4px 4x 0 0;
}

/* 上のメニューのタイトルの部分 */
.menu li a.active {
  background: #333;
  color: #fff;
}

/* 上のメニューのタイトルの部分のactiveクラスがついてない部分の動き */
/* hoverされるとactiveクラスがついてない部分は「opacity」によって薄くなる */
.menu li a:not(.active):hover {
  opacity: 0.5;
  transition: opacity 0.4s;
} 

/* menuの内容部分のactiveクラスがついている部分の見た目の部分 */
.content.active {
  background: #333;
  color: #fff;
  min-height: 150px;
  padding: 12px;
  display: block;
}

/* menuの内容部分のactiveクラスがついていない部分はdisplayで最初は見えなくしておく */
.content {
  display: none;
}

このような感じに見た目がなります。
スクリーンショット 2021-03-22 16.51.44.png

JSを実装していく

その前に、「data」属性を使ってmenuのタイトル部分と内容の部分を紐づけていきます

index.html
<div class="container">
    <ul class="menu">

<!-- こちらには「data-id」をつける -->
      <li><a href="#" class="active" data-id="about">サイトの概要</a></li>
      <li><a href="#" data-id="service">サービス内容</a></li>
      <li><a href="#" data-id="contact">お問い合わせ</a></li>
    </ul>

<!-- こちらには「id」:紐付けたい「data-id」と同じ名前をつける -->
    <section class="content active" id="about">
      サイトの概要。サイトの概要。サイトの概要。サイトの概要。
      サイトの概要。サイトの概要。サイトの概要。サイトの概要。
    </section>

    <section class="content" id="service">
      サービス内容。サービス内容。サービス内容。サービス内容。
      サービス内容。サービス内容。サービス内容。サービス内容。
    </section>
    <section class="content" id="contact">
      お問い合わせ。お問い合わせ。お問い合わせ。お問い合わせ。
      お問い合わせ。お問い合わせ。お問い合わせ。お問い合わせ。
    </section>
  </div>


最初に付けていたactiveクラスを外して表示したいmenuタイトルとmenuの内容部分に付けていくという実装をしていきます。
しかし!別々ではなくタイトル部分と内容部分を紐付けする必要があります。

js/main.js
'use strict';

{
  const menuItems = document.querySelectorAll('.menu li a');
  const contents = document.querySelectorAll('.content');

/* ページ遷移するのをキャンセルしてeventオブジェクトを渡す:デフォルトの動作をキャンセル */
  menuItems.forEach(clickedItem => {
    clickedItem.addEventListener('click', e => {
      e.preventDefault();

/* menuタイトル部分:「forEach」でactiveクラスを外した後にclickしたmenuタイトルにactiveクラスをつける */
      menuItems.forEach(item => {
        item.classList.remove('active');
      });
      clickedItem.classList.add('active');

/* menu内容部分:こちらでもactiveクラスを外して、その後にidを
取得しactiveクラスをつける */
      contents.forEach(content => {
        content.classList.remove('active');
      });
      document.getElementById(clickedItem.dataset.id).classList.add('active');
    });
  });
}

完成!!

完成したものをまとめるとこのような感じになります。

index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Tab Menu</title>
  <link rel="stylesheet" href="css/styles.css">
</head>
<body>
  <div class="container">
    <ul class="menu">
      <li><a href="#" class="active" data-id="about">サイトの概要</a></li>
      <li><a href="#" data-id="service">サービス内容</a></li>
      <li><a href="#" data-id="contact">お問い合わせ</a></li>
    </ul>

    <section class="content active" id="about">
      サイトの概要。サイトの概要。サイトの概要。サイトの概要。
      サイトの概要。サイトの概要。サイトの概要。サイトの概要。
    </section>

    <section class="content" id="service">
      サービス内容。サービス内容。サービス内容。サービス内容。
      サービス内容。サービス内容。サービス内容。サービス内容。
    </section>
    <section class="content" id="contact">
      お問い合わせ。お問い合わせ。お問い合わせ。お問い合わせ。
      お問い合わせ。お問い合わせ。お問い合わせ。お問い合わせ。
    </section>
  </div>


  <script src="js/main.js"></script>
</body>
</html>

css/styles.css
body {
  font-size: 14px;
}

.container {
  margin: 30px auto;
  width: 500px;
}

.menu {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
}

.menu li a {
  display: inline-block;
  width: 100px;
  text-align: center;
  padding: 8px 0;
  color: #333;
  text-decoration: none;
  border-radius: 4px 4x 0 0;
}

.menu li a.active {
  background: #333;
  color: #fff;
}

.menu li a:not(.active):hover {
  opacity: 0.5;
  transition: opacity 0.4s;
} 

.content.active {
  background: #333;
  color: #fff;
  min-height: 150px;
  padding: 12px;
  display: block;
}

.content {
  display: none;
}

js/main.js
'use strict';

{
  const menuItems = document.querySelectorAll('.menu li a');
  const contents = document.querySelectorAll('.content');

  menuItems.forEach(clickedItem => {
    clickedItem.addEventListener('click', e => {
      e.preventDefault();

      menuItems.forEach(item => {
        item.classList.remove('active');
      });
      clickedItem.classList.add('active');

      contents.forEach(content => {
        content.classList.remove('active');
      });
      document.getElementById(clickedItem.dataset.id).classList.add('active');
    });
  });
}

こちらもつけ外しで実装できました!!

Image from Gyazo

最後に

こちらもアニメーションを変えたり、色を変えたりするともう少し違った感じにできますね!!
内容も最初は隠したい場合は「opacity」が「display」を使用して隠したりの実装もしていく感じかな。
今日はここまでです。

お疲れ様でした!!

参考

データ属性についてです↓

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AND条件とOR条件

AND条件
(a && b && c)
→faltyなモノが式の途中にあった場合には、その値を条件の結果として返す。
→なかった場合には最後の条件、その値を条件の結果として返す。

(a && b)
→左側がtrueなら右側を返す。

OR条件
(a || b || c)
→式の途中でtruthyが見つかった時点で、その値を条件の結果として返す。
→なかった場合には最後の条件、その値を条件の結果として返す。

(a || b)
→左側がfalseなら右側を返す。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【kintone】グラフ設定APIで分類する項目を変更する

前回はグラフ設定APIのPUTでグラフの種類変更やグラフの新規追加を試してみました。
今回は「分類する項目」の大、中、小項目を変更してみたいと思います。
(処理成功のアラートや、エラー処理等は適宜書いてください~。)

アプリの準備

出来上がりのフォームはこんな感じ

image.png

フォーム

文字列(1行)とスペースを図のように設置します。
ボタンやドロップダウンフィールドはスペースに設置していきます。

フィールドコードなどは表のとおり。

image.png

フィールド種類 フィールド名(コード) 備考
文字列(1行) アプリID
スペース spGet グラフ設定取得ボタン用
スペース spDropDown グラフ選択ドロップダウン用
スペース spGraphSettings 分類する項目(大、中、小)用
スペース spPut グラフ設定更新ボタン用

kintone UI Component v1

ドロップダウンやボタンを使いやすく&ちょっと可愛くしたい!
というわけでkintone UI Component v1を使用します。

↓こちらを熟読して、CDNまたはダウンロードしたものをJavaScript /CSS でカスタマイズに設定しておきましょう。

JavaScript

というわけで、大きく分けて、下記4つの機能をプログラミングしていきます。

  1. ボタンやドロップダウンを設置する
  2. グラフの設定を取得する
  3. グラフを選択したら、選択したグラフの設定を取得
  4. グラフの設定を更新する

新規レコード追加や編集画面でうごくようにします。

kintone.events.on(
  ["app.record.create.show", "app.record.edit.show"],
  (event) => {
    //コードを書いていくところ 
  });

ボタンやドロップダウンを設置する

スペースフィールドにボタンやドロップダウンを設置していきます。

ボタンの設置

const spGet = kintone.app.record.getSpaceElement("spGet");
const spPut = kintone.app.record.getSpaceElement("spPut");
const btnGet = new Kuc.Button({
  text: "グラフの設定取得",
  type: "submit",
});

const btnPut = new Kuc.Button({
  text: "グラフの設定更新",
  type: "submit",
});
spGet.appendChild(btnGet);
spPut.appendChild(btnPut);

グラフ選択ドロップダウン

itemsは後で入れるので空にしておきます。

const spDropDown = kintone.app.record.getSpaceElement("spDropDown");
const drSelGraph = new Kuc.Dropdown({
  label: "グラフ",
  requiredIcon: true,
  items: [],
  visible: true,
  disabled: false,
});
spDropDown.appendChild(drSelGraph);

分類する項目(大、中、小)ドロップダウン

//グラフ設定表示
const spGraphSettings = kintone.app.record.getSpaceElement(
  "spGraphSettings"
);
const drGroups = [
  new Kuc.Dropdown({
    label: "分類する項目(大)",
    requiredIcon: true,
    items: [],
    visible: true,
    disabled: false,
  }),
  new Kuc.Dropdown({
    label: "分類する項目(中)",
    requiredIcon: true,
    items: [],
    visible: true,
    disabled: false,
  }),
  new Kuc.Dropdown({
    label: "分類する項目(小)",
    requiredIcon: true,
    items: [],
    visible: true,
    disabled: false,
  }),
];

drGroups.forEach((d) => {
  spGraphSettings.appendChild(d);
});

グラフの設定取得ボタン

アプリIDを入力して、グラフの設定を取得するボタンをクリックしたら、

  1. グラフ選択用のドロップダウンの候補にグラフ情報をセット
  2. 分類する項目(大、中、小)のドロップダウン選択候補にアプリのフィールドをセット

という動きをさせます。

// グラフ情報
let graphSetting;

//グラフ設定取得ボタンクリック
btnGet.addEventListener("click", async () => {
  const obj = kintone.app.record.get();

  //グラフ選択用のドロップダウンにグラフ情報をセット
  const body = {
    app: obj.record.アプリID.value,
  };

  //APIでグラフの設定をゲット
  graphSetting = await kintone.api(
    kintone.api.url("/k/v1/app/reports", true),
    "GET",
    body
  );

  //グラフ一覧をグラフ選択用ドロップダウンにセット
  const gItems = [];
  Object.keys(graphSetting.reports).forEach((g) => {
    gItems.push({ label: g, value: g });
  });
  drSelGraph.items = gItems;

  // 分類する項目ドロップダウンにセットする、アプリのフィールドを取得
  const fields = await kintone.api(
    kintone.api.url("/k/v1/app/form/fields", true),
    "GET",
    body
  );
  const groupsItems = [{ label: "", value: "" }]; //空の設定を入れておく
  Object.keys(fields.properties).forEach((g) => {
    // カテゴリーやステータスは弾きましょう
    if (
      fields.properties[g].type === "CATEGORY" ||
      fields.properties[g].type === "STATUS"
    ) {
      return;
    }

    //サブテーブル内のフィールドも取得
    if (fields.properties[g].type === "SUBTABLE") {
      Object.keys(fields.properties[g].fields).forEach((tg) => {
        groupsItems.push({
          label: fields.properties[g].fields[tg].label,
          value: fields.properties[g].fields[tg].code,
        });
      });
    } else {
      groupsItems.push({
        label: fields.properties[g].label,
        value: fields.properties[g].code,
      });
    }
  });
  // 分類する項目大中小3つのドロップダウンにフィールドをセットする
  drGroups.forEach((d) => {
    d.items = groupsItems;
  });

});

グラフ選択したとき

グラフ選択をした時は、分類する項目大中小のドロップダウンに設定のフィールドをセットします。
(日付、時刻等の「分類する項目の時間単位」は今回無視しています?)

drSelGraph.addEventListener("change", (event) => {
  // 一旦全ドロップダウンをクリア
  drGroups.forEach((d) => {
    d.value = "";
  });
  // グラフ設定で呼び出した分類する項目のフィールドをセット
  graphSetting.reports[drSelGraph.value].groups.forEach((g, idx) => {
    drGroups[idx].value = g.code;
  });
});

グラフ設定更新ボタンクリック

//グラフ設定更新ボタンクリック
btnPut.addEventListener("click", async () => {
  const obj = kintone.app.record.get();
  const bodyGet = {
    app: obj.record.アプリID.value,
  };
  //グラフAPIでグラフの設定を呼び出す
  const graphSetting = await kintone.api(
    kintone.api.url("/k/v1/app/reports", true),
    "GET",
    bodyGet
  );

  // 分類する項目をupするグラフ設定にセット
  const groupsPut = [];
  drGroups.forEach((g, idx) => {
    if (!!g.value) {
      groupsPut.push({
        code: g.value,
      });
    }
  });
  graphSetting.reports[drSelGraph.value].groups = groupsPut;
  const bodyPut = {
    app: obj.record.アプリID.value,
    reports: graphSetting.reports,
  };

  // 後は前回と同じ
  const resp = await kintone.api(
    kintone.api.url("/k/v1/preview/app/reports", true),
    "PUT",
    bodyPut
  );
  await kintone.api(
    kintone.api.url("/k/v1/preview/app/deploy", true),
    "POST",
    {
      apps: [
        {
          app: obj.record.アプリID.value,
        },
      ],
    }
  );

});

全体的なコードは以下です。

動作確認とまとめ

分類する項目の中項目と小項目は必須じゃないほうがいいですね?

graphgetput.gif

分類する項目が日付や日時フィールドの場合の時間単位が選択できない(更新もできない)などあります。
集計方法やソートについても似たような方法で実装することはできると思いますので、ぜひチャレンジしてみてくださいっ!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ProxyベースのReact状態管理ライブラリ「Valtio」v1リリース

Valtioがv1.0.0になりました! :tada:

ぜひ試しにでも使ってみてください。

基本的に不具合等の対処が一通り終わって安定してきたのでv1になったのですが、一つだけ隠し機能だったものがオープンになりました。

useProxyマクロ

v0.7.1まではuseProxyは本体から提供されていましたが、v0.8.0からはuseSnapshotに改名しました。中身は変わっていません。

代わりにuseProxyはマクロとして提供されるようになりました。babel-plugin-macrosを使っているのですが、Create React Appを使っている場合はすでに組み込まれています。

このuseProxyマクロを使うと、

import { useProxy } from 'valtio/macro'

const Component = () => {
  useProxy(state)
  return (
    <div>
      {state.count}
      <button onClick={() => ++state.count}>+1</button>
    </div>
  )
}

と言うコードが、次のように変換されます。

import { useSnapshot } from 'valtio'

const Component = () => {
  const snap = useSnapshot(state)
  return (
    <div>
      {snap.count}
      <button onClick={() => ++state.count}>+1</button>
    </div>
  )
}

useProxyマクロを使うと、snapshotをほとんど意識せずにコーディングすることができます。snapshotの概念がなくなるわけではありませんが、snapshotの扱いは癖があるので、マクロが使えるケースでは役立つでしょう。ちなみに、eslint-plugin-valtioもあります。

おわりに

valtioのv1リリースが完了したので、次はjotai。こっちは大物。
https://github.com/pmndrs/jotai/issues/333

React開発者向けオンラインサロン「React Fan」の紹介

最後に、私が主催している「React Fan」というコミュニティをお知らせします。テキストチャットでコミュニケーションできるSlackのワークスペースを用意しています。Slackへの参加は無料ですので、ご興味がある方はぜひご参加ください。詳しくは、下記のページをご参照ください。

React開発者向けオンラインサロン「React Fan」の入り口ページ

Slackへの招待リンクも上記ページにあります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScript コンテキストとスコープ

  変数や関数の実行コンテキストでアクセスできるデータ及び動きが決められる。そして実行コンテキスト毎に「変数オブジェクト」という物を紐付けている、実行コンテキストの中で宣言した変数と関数はその「変数オブジェクト」の中に存在している。外部では直接「変数オブジェクト」をアクセスできないが、内部で使われている。

  ブラウザの中ではグローバルコンテキストがwindowオブジェクトです。そこで、letとconst以外で宣言したグローバル変数と関数はwindowオブジェクトのプロパティとメソッドになるわけです。

コンテキストスタック(context stack)

  すべての関数呼び出しには独自のコンテキストが存在する。プログラムの実行が関数の中に入った時、該当関数のコンテキストがコンテキストスタックの中にpushされる。関数の実行が終わったら該当関数のコンテキストがコンテキストスタックからpopされる。これによってプログラムのコントロール権が一個前のコンテキストに移動する。よって今現在プログラムのコントロール権を持っているのはコンテキストスタックへ最後にpushされたコンテキストです。
  上記で見たの通り、JavaScriptプログラムの実行の流れがこのコンテキストスタックによって制御されている。

  図で見るとこんな感じです
image.png

スコープチェーン(scope chain)

  コンテキストのプログラムが実行する際変数オブジェクトの「スコープチェーン」を構築する、このスコープチェーンによって各コンテキストのプログラムがアクセスする変数や関数の順番を決める。今実行中のコンテキストの変数オブジェクトが常にスコープチェーンの先頭にあります。
  グローバル変数オブジェクトが常にスコープチェーンの最後にあります。
  コンテキストのプログラムを実行する時、利用する変数や関数をこのスコープチェーンの先頭から最後のグローバル変数オブジェクトまで検索していきます、見つけたらそこで終了、見つけられなかったらエラーになる。

スコープチェーンの例
var globalVar = "global";
function outterFunc(){
  let outVar = "outterFunc";

  function innerFunc(){
     let innerVar = "innerFunc";

     console.log(globalVar);  //アクセスできる
     console.log(outVar);  //アクセスできる
     console.log(innerVar);  //アクセスできる
  }
  innerFunc();
  console.log(globalVar);  //アクセスできる
  console.log(outVar);  //アクセスできる
  console.log(innerVar);  //アクセスできない
}
outterFunc();
console.log(globalVar);  //アクセスできる
console.log(outVar);  //アクセスできない
console.log(innerVar);  //アクセスできない

図でみるとこんな感じです
image.png
  矢印のように内部のコンテキストはスコープチェーンを使って外部コンテキストの変数や関数を検索してアクセスする、逆の外部から内部をアクセスすることはできない。

「catch」と「with」におけるスコープチェーン

実行コンテキストは主にグローバルコンテキストと関数コンテキストの二種類です。
だが「try/catchのcatchブロック」と「with」の場合はスコープチェーンの先頭に一時的なコンテキストを追加する。

  • 「catch」の場合、追加したコンテキストには例外情報が入っている、この例外情報は「catch」の中でしかアクセスできないわけはスコープチェーンから考えると分かり易い(IE9以前ではこのように実装されていないので、catch以外のところでもアクセスできる)。
  • 「with」の場合もスコープチェーンから考えると{}の中で変数をアクセスするようにwithで指定した対象オブジェクトのプロパティを簡単にアクセス出来ちゃう。 image.png 図のように実行プログラムが「catch」又は「with」に合った際、スコープチェーンの先頭に一時的なコンテキストを追加される。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

(gas) doPostで「現在、ファイルを開くことができません。」というレスポンスが返り処理が失敗する

doPostで「現在、ファイルを開くことができません。」というレスポンスが返ってくる

GasのWebアプリをデプロイしたときに、doPostの処理を下記のように実装していたところ、

function doPost(e) {
  //do something
  //{ok:true} を返す。
  return ContentService.createTextOutput(JSON.stringify({ok:true}, null, 2))
          .setMimeType(ContentService.MimeType.JSON);
} 

レスポンスが下記のようになってしまう現象がありました。
(さらに、doPostの処理自体も実行されませんでした。)

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta name="description" content="ウェブ ワープロ、プレゼンテーション、スプレッドシート">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=0">
    <link rel="shortcut icon" href="//docs.google.com/favicon.ico">
    <title>ページが見つかりません</title>
    <meta name="referrer" content="origin">
    <link href="//fonts.googleapis.com/css?family=Product+Sans" rel="stylesheet" type="text/css" nonce="dyW2qxOghsguMpKStDe2JA">
    <style nonce="dyW2qxOghsguMpKStDe2JA">/* Copyright 2021 Google Inc. All Rights Reserved. */
.goog-inline-block{position:relative;display:-moz-inline-box;display:inline-block}* html .goog-inline-block{display:inline}*:first-child+html .goog-inline-block{display:inline}#drive-logo{margin:18px 0;position:absolute;white-space:nowrap}.docs-drivelogo-img{background-image:url('//ssl.gstatic.com/images/branding/googlelogo/1x/googlelogo_color_116x41dp.png');background-size:116px 41px;display:inline-block;height:41px;vertical-align:bottom;width:116px}.docs-drivelogo-text{color:#000;display:inline-block;opacity:0.54;text-decoration:none;font-family:'Product Sans',Arial,Helvetica,sans-serif;font-size:32px;text-rendering:optimizeLegibility;position:relative;top:-6px;left:-7px;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}@media (-webkit-min-device-pixel-ratio:1.5),(min-resolution:144dpi){.docs-drivelogo-img{background-image:url('//ssl.gstatic.com/images/branding/googlelogo/2x/googlelogo_color_116x41dp.png')}}</style><style type="text/css" nonce="dyW2qxOghsguMpKStDe2JA">body {background-color: #fff; font-family: Arial,sans-serif; font-size: 13px; margin: 0; padding: 0;}a, a:link, a:visited {color: #112ABB;}</style><style type="text/css" nonce="dyW2qxOghsguMpKStDe2JA">.errorMessage {font-size: 12pt; font-weight: bold; line-height: 150%;}
    </style>
  </head>
  <body>
    <div id="outerContainer">
      <div id="innerContainer">
        <div style="position: absolute; top: -80px;">
          <div id="drive-logo">
            <a href="/">
              <span class="docs-drivelogo-img" title="Google ロゴ"></span>
              <span class="docs-drivelogo-text">&nbsp;ドライブ</span>
            </a>
          </div>
        </div>
        <div align="center">
          <p class="errorMessage" style="padding-top: 50px">現在、ファイルを開くことができません。</p>
          <p>アドレスを確認して、もう一度試してください。</p>
          <div style="background: #F0F6FF; border: 1px solid black; margin-top: 35px; padding: 10px 125px; width: 300px;">
            <p><strong>あれもこれも Google ドライブで</strong></p>
            <p> Google ドライブにはドキュメント やスプレッドシート、プレゼンテーションなどを簡単に作成、保存してオンラインで共有できるアプリが揃っています。</p><p>詳細 は<a href="https://drive.google.com/start/apps">drive.google.com/start/apps</a>をご覧ください。</p></div></div></div></div></body><style nonce="dyW2qxOghsguMpKStDe2JA">html {height: 100%; overflow: auto;}body {height: 100%; overflow: auto;}#outerContainer * Connection #1 to host script.googleusercontent.com left intact
{margin: auto; max-width: 750px;}#innerContainer {margin-bottom: 20px; margin-left: 40px; margin-right: 40px; margin-top: 80px; position: relative;}</style></html>

原因:doPostで ContentService.createTextOutput を返り値にしていたためだった

doGetで使用できていたため、doPostでもContentService.createTextOutput で結果をjsonとして返そうとしたところ失敗しました。

解決方:doPostは値を返さないようにする。

function doPost(e) {
  // do something
  // 値を返す処理をしない
} 

これでdoPostの処理が成功するようになりました。
以上です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

(gas) 公開SpreadSheetを簡易databaseにするApps ScriptをOOPで書く

SpreadSheetを簡易databaseにしたAPIができないか

調べたらできそうだったので下記の資料を参考に実装してみました。

参考にさせていただいた資料

Google Sheets にデータを追加、更新、削除する Web API をサクッと作る
https://qiita.com/takatama/items/e5cb83012d14c0094a79
【GAS】スプレッドシート内の全シートへのリンク一覧を作る
https://qiita.com/okNirvy/items/d1a2f4918cff8e63dcac
Google Apps ScriptのdoPostでJSONなパラメータのPOSTリクエストを受ける
https://qiita.com/shirakiya/items/db22de49f00710478cfc

JSのOOP

oopでjsを書く練習をしたかったため、functionでオブジェクトを定義するoopで実装しました。

実装するAPIの仕様

  • GET で指定したシートの全レコードを取得する
  • POST で指定したシートに新しい行を挿入する。

(削除なども実装できそうでしたが今回は割愛します。)
20210321_1.png

SpreadSheetDatabaseの実装

スプレッドシート管理オブジェクト「SpreadSheetDatabase」の実装は下記のとおり行いました。
targetSheetNameで操作対象シートオブジェクトを初期化して、レコードを取得、追加できるようにします。

/**
 * スプレッドシート管理
 * @param targetSheetName:string
 */
function SpreadSheetDatabase(targetSheetName) {
  const _this = this;

  // targetSheetNameでシートを初期化
  const book = SpreadsheetApp.getActive();
  const sheet = book.getSheets().find(sheet => sheet.getName() === targetSheetName);
  if(!sheet){
    throw new Error('シートが存在しません');
  }

  // 1行目はカラムキー行
  _this.columnKeys = function () {
    return allRows().splice(0, 1)[0];
  };

  // 2行目以降はレコード行
  _this.allRecords = function () {
    const records = allRows();
    return records.slice(1, records.length);
  };

  // 全行取得
  function allRows() {
    return sheet.getDataRange().getValues();
  };

  // 行の挿入
  _this.insertRow = function(row) {
    if(!Array.isArray(row)){
      return;
    }
    return sheet.appendRow(row);
  };
}



SpreadSheetAPIControllerの実装

APIコントローラーオブジェクト「SpreadSheetAPIController」の実装は下記のとおり行いました。
handleGet 、handlePostをpublicメソッドとして持ち、それぞれリクエストされたシートのSpreadSheetDatabaseオブジェクトを用いてデータを操作します。
(eオブジェクトからdataを取得する個所などの共通モジュールをAppUtilsにまとめています。)


/**
 * APIコントローラー
 */
function SpreadSheetAPIController() {
  const _this = this;
  let db;

  /**
   * 初期化
   * @param e:request変数
   * @param method:string
   */
  function initDB(e, method='GET') {
    let sheet = getTargetSheetNameFromData(
      parseDataWithMethodName(e, method)
    );
    // 指定スプレッドシートの管理インスタンスを生成
    db = makeTargetSpreadSheetDatabase(
      sheet
    );
  }

  /**
   * @method GET
   */
  _this.handleGet = function (e) {
    initDB(e, 'get');
    return AppUtils.convertRowsToAPIResult(
      db.allRecords(),
      db.columnKeys()
    );
  }
  /**
   * @method POST
   */
  _this.handlePost = function(e) {
    initDB(e, 'post');
    db.insertRow(
      AppUtils.convertPostDataToRow(
        AppUtils.parsePostData(e),
        db.columnKeys()
      )
    );
  }

  /** シート名が見つからないかった際の使用シート名 */
  const DEFAULT_SHEET_NAME = 'シート1';

  /**
   * 指定されたシート名を取得
   * @param data:object
   * @return sheet:string
   */
  function getTargetSheetNameFromData(data) {
    return (data && data.sheet) ? data.sheet : DEFAULT_SHEET_NAME;
  }
  /**
   * 指定されたmethod名に対応してdataを取得
   * @param e:request変数
   * @param method:string
   * @return data:object
   */
  function parseDataWithMethodName(e, method = 'GET') {
    if(method.toLowerCase() == 'get') {
      return AppUtils.parseGetParams(e);
    }
    if(method.toLowerCase() == 'post') {
      return AppUtils.parsePostData(e);
    }
  }

  /**
   * 指定したスプレッドシートの管理インスタンスを作成
   * @return db:SpreadSheetDatabase
   */
  function makeTargetSpreadSheetDatabase(sheet) {
    return new SpreadSheetDatabase(sheet);
  }
}


/**
 * utilities
 */
const AppUtils = {

  parseGetParams(getE) {
    return getE.parameter;
  },
  parsePostData(postE) {
    return JSON.parse(postE.postData.getDataAsString());
  },
  convertPostDataToRow(postData, keys) {
    return keys.map(key=>{
      const value = postData[key];
      if (value instanceof Date) {
        return Utilities.formatDate(value, 'Asia/Tokyo', 'yyyy/MM/dd HH:mm:ss');
      }
      if (typeof value === 'object') {
        return JSON.stringify(value);
      }
      return value;
    });
  },
  convertRowsToAPIResult(rows, keys) {
    const result = rows.map(row => {
      const obj = {};
      row.map((item, index) => {
        obj[String(keys[index])] = String(item);
      });
      return obj;
    });
    return result;
  },
}

Response の実装

ContentsServiceのcreateTextOutput (https://developers.google.com/apps-script/reference/content/content-service) を使い、APIの結果のjsonを返します。
失敗時は、Abortでerror.messageを受け取ってResponseを作ります。
GASの実装は初めてだったのですが、Web Appを作る場合、doGet、doPostの返り値として任意statuscodeを持つHTTPResponseを作ることができないようでした。
つまり、400エラー500エラーをAPIが任意に送信できないようです。
(商用サービスの実装でなく、学習用の簡易のDatabaseとする目的のためなら問題はないと思います。)

/**
 * @param data:object
 * @return output:TextOutput
 */
function Response(data) {
  return ContentService.createTextOutput(JSON.stringify(data, null, 2))
          .setMimeType(ContentService.MimeType.JSON);
}

/**
 * @param error:Error
 * @param etc:any
 * @return output:TextOutput
 */
function Abort(error, etc) {
  const errorResponse = {
    error: {
      message:error.message,
    }
  }
  errorResponse.etc = etc;
  return Response(errorResponse);
}

doGet、doPostの実装

const contoller = new SpreadSheetAPIController();

function doGet(e) {
  try{
    let result = contoller.handleGet(e);
    return Response(result);
  } catch(error) {
    return Abort(error);
  }
}

function doPost(e) {
  contoller.handlePost(e);
}

デプロイ後のAPIの実行結果

GET

  • doGetが実行され、全行が取得されます。
 curl -L -X POST https://script.google.com/macros/s/GASのWebサービスのデプロイ時のURL/exec
[
  {
    "task": "a",
    "status": "b",
    "etc": "c"
  },
  {
    "task": "牛乳をあげる",
    "status": "in_progress",
    "etc": ""
  }
]
  • POST

doPostが実行され、新規行が追加されます。

curl -X POST -H "Content-Type: application/json" -d '{"status": "AAA", "task":"BBB"}' https://script.google.com/macros/s/GASのWebサービスのデプロイ時のURL/exec
<!DOCTYPE html><html><head><link rel="shortcut icon" href="//ssl.gstatic.com/docs/script/images/favicon.ico"><title>エラー</title><style type="text/css" nonce="hzMivqfkAYlouW/btFBSyw">body {background-color: #fff; margin: 0; padding: 0;}.errorMessage {font-family: Arial,sans-serif; font-size: 12pt; font-weight: bold; line-height: 150%; padding-top: 25px;}</style></head><body style="margin:20px"><div><img alt="Google Apps Script" src="//ssl.gstatic.com/docs/script/images/logo.png"></div><div style="text-align:center;font-family:monospace;margin:50px auto 0;max-width:600px">スクリプトが完了しま したが、何も返されませんでした。</div></body></html>

20210321_2.png

最終的な実装

以上です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【jquery,js】指定したURLを開く [window.open,location.href] [js14_20210322]

処理の概要

処理のフロー:

 (1)テキストボックスのURLを取得する
 (2)指定の方法でURLへアクセスする
※遷移出来ない場合は、デフォルトでサーバエラーとなる

画面イメージ

画像1


補足)それぞれ指定のボタンを押下するとテキストボックス内にあるURLに別タブや別ウィンドウで遷移します。

ソースコード

index.html
<body>
    <div class="inputtext" id="container">
        <input type="text" id="urlInput" value="https://www.google.co.jp/" ><br>
        <input type="button" id="urlMoveButton" value="URLの移動をする"><br>
        <input type="button" id="urlTabOpenButton" value="別のたぶを開く"><br>
        <input type="button" id="urlWindowOpenButton" value="別のウィンドウを開く">
    </div>
</body>
main.js
$(function() {
    $("#urlMoveButton").click(function(){
        var urlMoveText = $("#urlInput").val();
        location.href = urlMoveText;
    });

    $("#urlTabOpenButton").click(function(){
        var urlOpenText = $("#urlInput").val();
        window.open(urlOpenText, "_blank");
    });

    $("#urlWindowOpenButton").click(function(){
        var urlOpenText = $("#urlInput").val();
        window.open(urlOpenText ,null, 'width=500, toolbar=yes , menubar=yes,scrollbars=yes');
    });
});

ポイント

html:
(1)特になし
js:
(1)window.openはオプションの指定方法で新しいタブ、またはウィンドウを指定することが出来る

参考資料

JavaScript(仕事の現場でサッと使える!デザイン教科書) p102

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【令和版】window.close() でタブが閉じない時の解決法

開発者「すいません、window.close()が動かないいんですけど…」

ワイ「コンソールになんか書いてないですか?」

Scripts may close only the windows that were opened by them.

開発者「ってでてます。」

ワイ「そこに書いてある通りですね。」

開発者「すみません、英語わかんなくて…」

ワイ「」


なぜ閉じないのかわからない人も、どうすればいいかと相談される人も、window.close()で消耗するのはもうおしまいにしましょう。

もう聞かれた時にURLを投げればいいだけにしておきたいので、この記事を残しておきます。

さて、まずはなぜ解決できないのかを知っていくために、先ほどの会話の中で出てきた英文の日本語訳を確認します。

Google翻訳:スクリプトは、スクリプトによって開かれたウィンドウのみを閉じることができます。

直訳なのでわかりづらいですが、要はJavaScriptで閉じることができるウィンドウは、JavaScriptで開かれたウィンドウのみであると書いてあります。

しかしながら、時々JavaScriptで開いてないウィンドウもwindow.close()で閉じることができる気がします。それはなぜでしょうか?

一旦冷静になって仕様を確認してみましょう。

What is window.close()

A browsing context is script-closable if it is an auxiliary browsing context that was created by a script (as opposed to by an action of the user), or if it is a top-level browsing context whose session history contains only one Document.

参考文献:https://html.spec.whatwg.org/multipage/window-object.html#dom-window-close

ざっくり要約すると、window.close()で閉じることができるのは次のパターンです。

  • JavaScriptによって開かれた場合
  • 履歴が1つしかないトップレベルブラウジングコンテキストの場合

それぞれ1つずつ見ていきます。

JavaScriptで開かれた場合

window.open()1によって開かれた場合がこれに該当します。

ちなみに、window.open()でも、featuresnoopenerが指定されていると「JavaScriptで開かれた場合」に該当しない振る舞いになります。
逆に、a[target="_blank"]でも[rel="opener"]を付与すれば、「JavaScriptで開かれた場合」に該当する振る舞いになります2

こうした実際の動きをみると、「JavaScriptによって開かれた」というよりも、opener3が存在する場合、と言った方が正しいかもしれませんね。

履歴が1つしかないトップレベルブラウジングコンテキストの場合

2つの要素が関係しているので、ここも1つずつ確認していきましょう。

履歴が1つしかない

window.history.length1のときを指します。

これは、タブ(ブラウジングコンテキスト)が開かれてから、どこページにも遷移していないということを表します。つまり、そのブラウジングコンテキストで「戻る」「進む」がどちらか1方向でもできる状態だと、履歴の長さは2以上になります。

if (window.history.length === 1) {
  // It is a top-level browsing context whose session history contains only one Document.
}

トップレベルブラウジングコンテキスト

  • window - 現在のブラウジングコンテキストのWindowProxy
  • window.parent - 親ブラウジングコンテキストのWindowProxy
  • window.top - トップレベルブラウジングコンテキストのWindowProxy

iframeなどでブラウジングコンテキストがネストしているとき、自身のブラウジングコンテキストがトップレベルかを確認するには、次のような方法で確認できます4

if (window === window.top) {
  // It is a top-level browsing context
}

逆に特別なことをしていなければ、常にトップレベルブラウジングコンテキストのはずです。

どちらの状況も満たせない時にウィンドウを閉じるにはどうしたらいいの?

残念ですが閉じることができません5

そういう場合は、そもそも閉じるボタンの実装を見送って「このタブを閉じてください」など、ユーザ自身にページを閉じるよう促す文言を掲載したりするしかないでしょう。

もし、多くのケースで閉じるボタンが有効ではあるものの、特定のケースでのみ前述の条件を満たせないような場合には、代替テキストを表示する方法もあります。

<button type="button" id="close">このボタンを押すとページが閉じるにぇ</button>

<script>
const closeBtn = document.getElementById('close');

closeBtn.addEventListener('click', function () {
  window.close();

  // `window.closed`を参照することでページが閉じられているかを確認できます。
  if (!window.closed) {
    this.textContent = '閉じるのに失敗したぺこだよ…';
  }
});
</script>

あるいは、もともと閉じることができるかを判定して、「閉じるボタン」と「ブラウザのUIで閉じるよう促す文言」を出し分けるのもいいかもしれません。

<p id="close">ブラウザの×ボタンを押してほしいんだワ</p>

<script>
const placeholder = document.getElementById('close');

if (
  window.opener ||
  (
    window === window.top &&
    history.length <= 1
  )
) {
  const closeBtn = document.createElement('button');

  closeBtn.type = 'button';
  closeBtn.textContent = '押したらページが閉じるっちゃま〜〜〜!';
  closeBtn.addEventListener('click', function () {
    window.close();
  });
  placeholder.replaceWith(closeBtn);
}
</script>

テスト用スペース

動いているのが確認したいという方向けにwindow.close()の動きをテストするためのページを用意してみました。

このページでは次の状態の違い、確認できます。

  • トップレベルブラウジングコンテキストかどうか
  • openerの存在
  • 履歴の長さ
  • Script closable(window.close()が利用可能か)

終わりに

わたしたちがwindow.close()閉じない問題を本当の意味で解決する方法は、そもそももっと上流工程を見直してwindow.close()が不要な同線設計になるようにするほかありません。

可能であれば、同線設計の話の部分にも食い込んで提案するようにしていきたいですね?


  1. https://developer.mozilla.org/ja/docs/Web/API/Window/open
    https://html.spec.whatwg.org/multipage/window-object.html#dom-open 

  2. Google Chrome 89、Firefox 86、Edge 89 で動作確認済み。Safari(14)ではhistoryの挙動が異なります。 

  3. https://developer.mozilla.org/ja/docs/Web/API/Window/opener 

  4. https://html.spec.whatwg.org/multipage/browsers.html#navigating-nested-browsing-contexts-in-the-dom 

  5. 通常のJavaScriptの範疇を超えない場合に限る(ブラウザ拡張を利用するなどはその限りではない) 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[JS]Hamburger menuについて

 はじめに

今後さらりと実装できるようになるように順序整理のために
記していきます。

 実装

実装していきます。
iconは↓こちらから取得します。

linkを貼ると、簡単にiconが取得できます。

スクリーンショット 2021-03-22 0.29.57.png

まずは形を作っていきます。

index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>My Site</title>

  <link href="https://fonts.googleapis.com/icon?family=Material+Icons"
    rel="stylesheet">

  <link rel="stylesheet" href="css/styles.css">
</head>
<body>
  <header>
    <div class="logo">
      <h1>LOGO</h1>
    </div>

    <div class="sp-menu">
      <span class="material-icons" id="open">menu</span>
    </div>
  </header>

  <div class="overlay">
    <span class="material-icons" id="close">close</span>
    <nav>
      <ul>
        <li><a href="#">Menu</a></li>
        <li><a href="#">Menu</a></li>
        <li><a href="#">Menu</a></li>
      </ul>
    </nav>
  </div>


  <main>
    <p>こんにちは。こんにちは。こんにちは。こんにちは。
      こんにちは。こんにちは。こんにちは。こんにちは。
    </p>
    <p>こんにちは。こんにちは。こんにちは。こんにちは。
      こんにちは。こんにちは。こんにちは。こんにちは。
    </p>
  </main>

  <script src="js/main.js"></script>
</body>
</html>

css/styles.css
body {
  margin: 0;
  font-family: Verdana, sans-serif;
}

header {
  display: flex;
  padding: 0 16px;
}

header h1 {
  margin: 0;
  font-size: 22px;
  line-height: 64px;
}

/* メニューの位置 */
.sp-menu {
  margin-left: auto;
}

/* menuのicon */
.sp-menu #open {
  font-size: 32px;
  line-height: 64px;
  cursor: pointer;
}

main {
  padding: 0 16px;
}

/* menuの中身の部分 */
.overlay {
  position: fixed;
  top: 0;
  bottom: 0;
  right: 0;
  left: 0;
  background: rgba(255,255,255,0.95);
  text-align: center;
  padding: 64px;
}

/* menuの「x」の部分 */
.overlay #close {
  position: absolute;
  top: 16px;
  right: 16px;
  font-size: 32px;
  cursor: pointer;
}

.overlay ul {
  list-style-type: none;
  margin: 0;
  padding: 0;
}

.overlay li {
  margin-top: 24px;
}

Menuたちは上に乗っているイメージ。

スクリーンショット 2021-03-21 19.53.01.png

menuの詳細を最初は「opacity」で隠してあげます。
「pointer-events」でmenuの詳細部分のポインターイベントを受け取らない設定にして、下の部分の「p」の部分に手を加えることができるようになります。

styles.css
.overlay {
  /* 前回記述したところに追加します */
  opacity: 0;
  pointer-events: none;
}

JSを実装していく

js/main.js
'use strict';

{
  const open = document.getElementById('open');
  const overlay = document.querySelector('.overlay');
  const close = document.getElementById('close');

/* 「open」の部分のiconをクリックしたらclassがつく */
  open.addEventListener('click', () => {
    overlay.classList.add('show');
    open.classList.add('hide');
  });

/* 「close」の部分のiconをクリックしたらclassが外れる */
  close.addEventListener('click', () => {
    overlay.classList.remove('show');
    open.classList.remove('hide');
  });
}

cssを追加

classをつけたり外したりする実装をJSで実装したので、
そのクラスをつけたらどのような動きにしたいのかをcssで追加の実装をしていきます。

css/styles.css
/* 追加のものだけ記述しています */

/* 「hide」クラスがつくとmenuのiconが隠れます */
.sp-menu #open.hide {
  display: none;
}

/* アニメーションの部分 */
.overlay {
  /* 他省略 */
  transition: opacity .6s;
}

/* 「show」がついたらする動き */
/* pointer-eventsをつけてあげる */
.overlay.show {
  opacity: 1;
  pointer-events: auto;
}

.overlay li {
  /* 他省略 */
  opacity: 0;
  transform: translateY(16px);
  transition: opacity .3s, transform .3s;
}

/* li:「show」がついたらする動き */
.overlay.show li {
  opacity: 1;
  transform: none;
}

/* 時間差で表示させる */
.overlay.show li:nth-child(1) {
  transition-delay: .1s;
}

.overlay.show li:nth-child(2) {
  transition-delay: .2s;
}

.overlay.show li:nth-child(3) {
  transition-delay: .3s;
}

そうするとこのような動きになります!!

Image from Gyazo

headerに追加

最後に大きさを変えると見た目が変わるように実装していきます。

index.html
<header>
    <div class="logo">
      <h1>LOGO</h1>
    </div>

  <!-- headerにmenuボタンを追加 -->
    <div class="pc-menu">
      <nav>
        <ul>
          <li><a href="#">Menu</a></li>
          <li><a href="#">Menu</a></li>
          <li><a href="#">Menu</a></li>
        </ul>
      </nav>
    </div>

css/styles.css
/* headerのmenuを隠しておく */
.pc-menu {
  display: none;
}

@media (min-width: 600px) {

/* 600pxからmenuを表示 */
  .pc-menu {
    display: block;
    margin-left: auto;
  }

  .pc-menu ul {
    list-style-type: none;
    margin: 0;
    padding: 0;
    display: flex;
  }

  .pc-menu a {
    display: block;
    width: 80px;
    line-height: 64px;
    text-align: center;
  }

  .pc-menu a:hover {
    background: #f2f2f2;
  }

  .sp-menu {
    display: none;
  }
}

このようにiconになったりmenuの文字が出てきたりと大きさで変わるように設定できました!!

Image from Gyazo

完成!!

完成したものをまとめるとこのような感じになります。

index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>My Site</title>
  <link href="https://fonts.googleapis.com/icon?family=Material+Icons"
    rel="stylesheet">
  <link rel="stylesheet" href="css/styles.css">
</head>
<body>
  <header>
    <div class="logo">
      <h1>LOGO</h1>
    </div>

    <div class="pc-menu">
      <nav>
        <ul>
          <li><a href="#">Menu</a></li>
          <li><a href="#">Menu</a></li>
          <li><a href="#">Menu</a></li>
        </ul>
      </nav>
    </div>

    <div class="sp-menu">
      <span class="material-icons" id="open">menu</span>
    </div>
  </header>

  <div class="overlay">
    <span class="material-icons" id="close">close</span>
    <nav>
      <ul>
        <li><a href="#">Menu</a></li>
        <li><a href="#">Menu</a></li>
        <li><a href="#">Menu</a></li>
      </ul>
    </nav>
  </div>


  <main>
    <p>こんにちは。こんにちは。こんにちは。こんにちは。
      こんにちは。こんにちは。こんにちは。こんにちは。
    </p>
    <p>こんにちは。こんにちは。こんにちは。こんにちは。
      こんにちは。こんにちは。こんにちは。こんにちは。
    </p>
  </main>

  <script src="js/main.js"></script>
</body>
</html>

css/styles.css
body {
  margin: 0;
  font-family: Verdana, sans-serif;
}

header {
  display: flex;
  padding: 0 16px;
}

header h1 {
  margin: 0;
  font-size: 22px;
  line-height: 64px;
}

.sp-menu {
  margin-left: auto;
}

.sp-menu #open {
  font-size: 32px;
  line-height: 64px;
  cursor: pointer;
}

.sp-menu #open.hide {
  display: none;
}

main {
  padding: 0 16px;
}

.overlay {
  position: fixed;
  top: 0;
  bottom: 0;
  right: 0;
  left: 0;
  background: rgba(255,255,255,0.95);
  text-align: center;
  padding: 64px;
  opacity: 0;
  pointer-events: none;
  transition: opacity .6s;
}

.overlay.show {
  opacity: 1;
  pointer-events: auto;
}

.overlay #close {
  position: absolute;
  top: 16px;
  right: 16px;
  font-size: 32px;
  cursor: pointer;
}

.overlay ul {
  list-style-type: none;
  margin: 0;
  padding: 0;
}

.overlay li {
  margin-top: 24px;
  opacity: 0;
  transform: translateY(16px);
  transition: opacity .3s, transform .3s;
}

.overlay.show li {
  opacity: 1;
  transform: none;
}

.overlay.show li:nth-child(1) {
  transition-delay: .1s;
}

.overlay.show li:nth-child(2) {
  transition-delay: .2s;
}

.overlay.show li:nth-child(3) {
  transition-delay: .3s;
}

.pc-menu {
  display: none;
}

@media (min-width: 600px) {
  .pc-menu {
    display: block;
    margin-left: auto;
  }

  .pc-menu ul {
    list-style-type: none;
    margin: 0;
    padding: 0;
    display: flex;
  }

  .pc-menu a {
    display: block;
    width: 80px;
    line-height: 64px;
    text-align: center;
  }

  .pc-menu a:hover {
    background: #f2f2f2;
  }

  .sp-menu {
    display: none;
  }
}

js/main.js
'use strict';

{
  const open = document.getElementById('open');
  const overlay = document.querySelector('.overlay');
  const close = document.getElementById('close');

  open.addEventListener('click', () => {
    overlay.classList.add('show');
    open.classList.add('hide');
  });

  close.addEventListener('click', () => {
    overlay.classList.remove('show');
    open.classList.remove('hide');
  });
}

 最後に

アニメーションをもう少し触ってみて動きの幅を広げていきたいと思います!!

お疲れ様でした!!!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

物理演算エンジン matter.js で簡単な機構を作る

matter.js は JavaScript で動く物理演算エンジンです。ブラウザゲームとか作るのに便利です。今回はこれで回転運動を往復運動に変換する クランク機構 を作ってみようと思います。こんなものができます。

crank-2.gif

無限に見てられますね。これを構成する部品は、

  • 回転運動する円盤(赤)
  • 往復運動するピストン(青)
  • 両者を連結させる束縛(白)
  • ピストンを内包しているシリンダー(黒)

です。自動車であればエンジンの往復運動→タイヤの回転運動ですが、今回は回転→往復でやります。用途はジャンプ漫画『Dr.STONE』に出てきた水車で動く送風機みたいなのを想像してください。

matter.js はダウンロードして、同じディレクトリの index.html で以下のように呼び出すだけで動きます。簡単。

index.html
<body></body>
<script src='matter.js'></script>
<script src='script.js'></script>

script.js にはまず matter.js の雛形を作ります。Engine とか World とか Render がそれぞれ何をしているのかは僕もよく知らないので公式ドキュメントを見ましょう。

script.js
// aliases
const Engine = Matter.Engine,
    Render = Matter.Render,
    World = Matter.World,
    Constraint = Matter.Constraint,
    Body = Matter.Body,
    Bodies = Matter.Bodies

const engine = Engine.create()
const render = Render.create({
    element: document.body,
    engine: engine,
})

engine.world.gravity.y = 0 // 今回は重力を使わない

/* ここにコードを追加していく */

Engine.run(engine)
Render.run(render)

真っ黒な画面が表示されます。次に円盤を作ります。

const disk = Bodies.circle(200, 200, 50)
World.add(engine.world, [disk])

円盤を固定する束縛 Constraint を設定します。束縛は物体と空間、または物体同士を一定距離で固定することができます。今回は円盤の中心を空間に固定します。

const anchor = Constraint.create({
    pointA: {x: 0, y: 0}, // 円盤の中心
    bodyA: disk,
    pointB: {x: 200, y: 200}, // 空間のこの位置に固定
    length: 0,
})
World.add(engine.world, anchor)

冒頭のGIFでは回転が見やすいようにホクロをつけていますが、コード上では省略しています。
次にピストンを作ります。これはただの長方形です。

const piston = Bodies.rectangle(500, 200, 100, 50)
World.add(engine.world, piston)

円盤とピストンの間に、同様の束縛を設定します。

const diskPistonJoint = Constraint.create( {
    pointA: {x: 30, y: 0}, // 円盤の中心から少しずらす
    bodyA: disk,
    pointB: {x: -40, y: 0},
    bodyB: piston,
    length: 230,
})
World.add(engine.world, diskPistonJoint)

最後にシリンダーを作ります。3個の長方形を組み合わせてひとつの物体にします。こちらは完全に動かないので isStatic: true フラグを立てて固定します。

const cylinder = Body.create( {
    parts: [
        Bodies.rectangle(450, 170, 300, 10), // 上壁
        Bodies.rectangle(450, 230, 300, 10), // 下壁
        Bodies.rectangle(605, 200, 10, 70), // 右壁
    ],
    isStatic: true, // 動かない
})
World.add(engine.world, cylinder)

できました。あとは円盤を回すだけです。物体に速度を与える時は以下のようにします。

Body.setAngularVelocity(disk, 0.1)

ただ matter.js は空気抵抗や摩擦があるので、放っておくとすぐに回転が止まってしまいます。今回は setInterval で0.1秒ごとに回転速度を再設定します。(もう少しスマートな方法がある気がするけど調べていない)

setInterval( () => {
    Body.setAngularVelocity(disk, 0.1)
}, 100)

できあがり。機構としてはこれで完成です。

_SS 2021-03-21 23.58.11.png

最終的なコードはこのようになります。

script.js
// aliases
const Engine = Matter.Engine,
    Render = Matter.Render,
    World = Matter.World,
    Constraint = Matter.Constraint,
    Body = Matter.Body,
    Bodies = Matter.Bodies

const engine = Engine.create()
const render = Render.create({
    element: document.body,
    engine: engine,
})

engine.world.gravity.y = 0 // 今回は重力を使わない

// 円盤を追加
const disk = Bodies.circle(200, 200, 50)
World.add(engine.world, disk)

// 円盤の中心を空間に固定
const anchor = Constraint.create({
    pointA: {x: 0, y: 0}, // 円盤の中心
    bodyA: disk,
    pointB: {x: 200, y: 200}, // 空間のこの位置に固定
    length: 0,
})
World.add(engine.world, anchor)

// ピストンを追加
const piston = Bodies.rectangle(500, 200, 100, 50)
World.add(engine.world, piston)

// ピストンと円盤の間に束縛を追加
const diskPistonJoint = Constraint.create( {
    pointA: {x: 30, y: 0}, // 中心から少しずらす
    bodyA: disk,
    pointB: {x: -40, y: 0},
    bodyB: piston,
    length: 230,
})
World.add(engine.world, diskPistonJoint)

// シリンダーを追加
const cylinder = Body.create( {
    parts: [
        Bodies.rectangle(450, 170, 300, 10),
        Bodies.rectangle(450, 230, 300, 10),
        Bodies.rectangle(605, 200, 10, 70),
    ],
    isStatic: true,
})
World.add(engine.world, cylinder)

// 円盤を回転
setInterval( () => {
    Body.setAngularVelocity(disk, 0.1)
}, 100)

Engine.run(engine)
Render.run(render)

色をつけたいときは、まず render の部分を以下のように変更します。

const render = Render.create({
    element: document.body,
    engine: engine,
    options: {
        wireframes: false,
        background: '#99cdee',
    }
})

_SS 2021-03-22 0.03.02.png

あとは下例のように各物体に render オプションをつけて、好きな色を設定していきましょう。

const disk = Bodies.circle(200, 200, 50, {
    render: {
        fillStyle: '#dd4444',
        strokeStyle: '#222222',
        lineWidth: 3,
    }
})

以上です。よき物理生活を。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む