20200330のJavaScriptに関する記事は30件です。

これからはじめる、Gatsbyのインストールから静的サイトのビルドまで

Gatsbyは次のWordpressとも言われている、Reactベースのオープンソースフレームワーク。

超高速なWebサイトやブログ、アプリを簡単に作ることができ、今最も注目されているCMSツールでもあります。

ここではGatsbyをこれからはじめる人のために、インストール〜静的サイトのビルドまでをサクッと解説していきます。

Node環境のインストール

まずは環境のチェック。nodeは11.10以降にする必要がある。

brew使ってたので、brewでnodeをアップデートする。

node入ってない人はここからダウンロードできる。

brew upgrade node

// nodeをインストールしてない場合
brew install node

インストールできたらnodeのバージョンチェック。

node -v
v13.11.0

Gatsbyのインストール

npm install -g gatsby-cli
gatsby new gatsby-site
cd gatsby-site
gatsby develop

http://localhost:8000にアクセス。ページが表示されればOK。

Home___Gatsby_Default_Starter.png

静的サイトのビルド(生成)

コマンドで自動的にファイルを作成してくれる。

gatsby build

ビルドすると、puglicフォルダに静的サイトが作成される。あとはサーバーにアップすればWebサイトを表示できる。

Gatsbyのスターターライブラリを使ったインストール

Gatsbyはデフォルトだけでなく、ブログやWebサイト、ポートフォリオサイトなどのスターターライブラリがあり、効率よく開発をスタートできる。

スターターライブラリ
https://www.gatsbyjs.org/starters/?v=2

ブログを作りたい場合はこちら。

gatsby new gatsby-starter-blog https://github.com/gatsbyjs/gatsby-starter-blog

Gatsbyのリソース

ドキュメントがしっかりしてるので、そこ見ればだいたいのことはわかる。

プラグインやライブラリなどもリンクされてるので、公式のリソース集はチェックしておきましょう。

最新情報はTwitterやRedditを活用。

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

オブジェクト指向プログラミングなremoveEventListenerの書き方

記事を書いたのにremoveEventListenerでまた躓いた。:confounded:

今回はオブジェクト指向プログラミング版。

失敗例

class X {
  click(e) {
    // thisがオブジェクトXを指すようにしたい
    console.log('click', this, e.target);
  }
  add() {
    document.addEventListener('click', (e) => this.click(e));
  }
  remove() {
    document.removeEventListener('click', (e) => this.click(e));
  }
}
var x = new X();
x.add();
x.remove();

(()=>{}) === (()=>{})(function(){}) === (function(){})がfalseを返すのでremoveEventListenerに無名関数を使用できない。

addEventListenerで指定した関数と同じ関数をremoveEventListenerで指定できていないのでイベントリスナーを削除できない。

イベントリスナー関数にthis.clickを指定するとremoveEventListenerで削除できるが、thisがdocumentを指し、e.targetもdocumentを指すので重複してしまう。

オブジェクト指向プログラミングでイベントリスナー関数は書けない?

ここで小一時間悩んだ。:thinking:

成功例

class X {
  constructor() {
    this.event_listeners = {};
  }
  click(e) {
    // thisがオブジェクトXを指すようにしたい
    console.log('click', this, e.target);
  }
  add() {
    this.event_listeners.click = this.click.bind(this);
    document.addEventListener('click', this.event_listeners.click);
  }
  remove() {
    document.removeEventListener('click', this.event_listeners.click);
  }
}
var x = new X();
x.add();
x.remove();

関数にthisをbind1して、その関数を保存して書けばいいと思う。:blush:


  1. this.event_listeners.click = (e) => this.click(e)と書いてもいい。addEventListenerがthisを書き変えるのでthis.event_listeners.click = function (e) { this.click(e) }とは書けない。 

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

2020年にフロントエンド開発者が作りたい9つのプロジェクト

こちらの記事は、Simon Holdorf 氏により2020年01月に公開された『 9 Projects to Inspire Front-End Developers in 2020 』の和訳です。
本記事は原著者から許可を得た上で記事を公開しています。

最初からはっきりと言ってしまいましょう。プログラミングに関する本を何冊読んでも、ビデオやポッドキャストを何本観たり聞いたりしても、もしあなたがより良い開発者になりたいのであれば、継続的に練習することは欠かせません。

フロントエンドの世界には、React、Angular、Vueなど数多くのフレームワークが存在します。どれも素晴らしいものばかりで、それらがなければフロントエンド開発は今のようにはなっていなかったでしょう。しかし、様々な特色を持つフレームワークでも、全てにおいて共通するのは、それらがすべてJavaScriptをベースにしているということです。そう、古き良きJavaScriptなのです!

今、Webを動かしているのは間違いなくJavaScriptです。また、フロントエンド開発者はベテランでも、キャリアを始めたばかりの新人であっても、JavaScript、HTML、CSSの基本は必須知識と言えます。

フレームワークは時代の流れによって流行り、また廃れていくものですが、JavaScriptは変わることがありません!

そこであなたのスキルをリフレッシュするべく、JavaScriptの新たな技術を学び、2020年の時代についていくための、素晴らしいプロジェクトを9つリストアップしました。これらはすべて純粋なJavaScript、HTML、CSSをベースにしており、ポートフォリオに追加したり、将来の雇用主に見せたり、また将来のプロジェクトのためのリファレンスとしてGitHubに残しておくこともできます。なお、これはJavaScriptについて一からすべてを学ぶためのものではありません。JavaScriptのA~Zまでを網羅したコースは他にもあります。ですがこれは一から物を作るためのものです。実際に手を動かして記憶力のトレーニングをしてみましょう。

読者の皆様が、すぐに取り掛かれるよう、参考までに、それぞれのチュートリアルプロジェクトに私の総評を載せています。ですがこれはあくまでも私の意見にすぎません。どのようにして学ぶのが最良か、そしてあなた自身の今のレベルが分かるのはあなただけです。是非ご自身で試してみることを強くお勧めします。

1. 瞑想アプリを作る

Youtubeリンク

構築するもの

瞑想のための環境音を再生するアプリケーションを構築します。ユーザーは、異なるタイマーとサウンドを選択できます。

総評

このチュートリアルでは、バニラJavaScriptを使用しています。インストラクターの声と指示がはっきりしており、理解しやすいです。それに加えてこのチュートリアルでは、JSを使ってサウンドとビデオを操作する方法が学べます。

星5/5

2. 仮想キーボードを作る

Youtubeリンク

構築するもの

ブラウザ上で動作するレスポンシブでタッチ操作にも対応した、バニラJS、HTML、CSSを使った仮想キーボードをゼロから構築します!

総評

私はこの企画の独創性にとても気に入りました。これまで私は仮想キーボードを作ったことがなく、JavaScript、HTML、CSSのみを使ってそれを作ることは、個人的にとてもすばらしい体験でした。インストラクターの声もはっきりしていて、説明もとても上手です。

星5/5

3. Eコマースショッピングカートを作る

Youtubeリンク

構築するもの

バニラJS、HTML、CSSを使用して、オンラインショップおよびEコマースサイトで使われるようなショッピングカートを構築します。ここでは商品情報を格納するためのヘッドレスCMSであるContentfulを利用しています。ContentfulやヘッドレスCMS全般の扱い方を学ぶことは、このチュートリアルの主要なポイントではありませんが、とても素晴らしいサービスなので、実際に動いているのを見て後悔することはないでしょう。

総評

このチュートリアル動画は非常に長いです。チュートリアルは印象的である一方で少し骨が折れる内容です。インストラクターの声ははっきりしていてわかりやすいです。彼の説明についていくのは少々難しい所もありますが、おそらくそれはこのプロジェクトの複雑さが故のものでしょう。

星4/5

4. 天気アプリを作る

Youtubeリンク

構築するもの

このプロジェクトでは、バニラJS、HTML、CSSを使用して天気アプリケーションを構築する方法について説明しています。ここではDark Sky APIを使用して気象情報を取得しています。このチュートリアルはAPIの操作方法を習得するための素晴らしい機会になるでしょう。

総評

このプロジェクトはポートフォリオにぴったりかもしれません。プロジェクト1をすでに受けた方なら、このインストラクターのことはご存知でしょう。このチュートリアルも非常に高いクオリティーで完成しています。彼は本当に分かりやすくて面白い教え方を知っているようです。

星5/5

5. Issueトラッカーを作る

Youtubeリンク

構築するもの

このチュートリアルでは、バニラJS、HTML、CSSを使用して(ソフトウェア製品などの)Issueを作成するためにどのようなWebサイトでも使用できる課題追跡ツールを構築します。これはポートフォリオなどに載せるのにも適しています。

総評

インストラクターは、あなたと一緒にこれから何を構築していくのかについて明確なコンセプトを持っています。彼の声は明瞭で理解しやすく、初心者に優しいプロジェクトになっています。

星4/5

6. PINパッドを作る

Youtubeリンク

構築するもの

このプロジェクトではブラウザ上で動作し、入力されたPINコードが正しいかどうかをチェックする機能を持つPINパッドを構築します。このチュートリアルでインストラクターは、バニラJS、HTML、CSSのみを使用しています。

総評

バニラJavaScriptだけで作れる物の可能性をとてもよく示している、非常に創造的なチュートリアルプロジェクトだと思います。インストラクターはすべてをわかりやすく、また付いて行きやすい様に説明してくれます。

星4/5

7. ランディングページを作る

Youtubeリンク

構築するもの

ここではBradが時間とユーザーの名前を表示するインタラクティブなランディングページの構築方法を説明しています。アプリケーションはLocalStorageを使用して名前を保存するため、これを確認しておくと便利です。

総評

Bradは、完全で優れたチュートリアルでよく知られています。このチュートリアルはかなり短く、単純化されていると言えますが、JavaScript、HTML、CSSのみを使用しています。

星4/5

8. じゃんけんゲームを作る

Youtubeリンク

構築するもの

ゲームを作ることは新しいことを学ぶ楽しい方法になり得ます。JavaScriptは、ブラウザベースのゲームを作成できる大きなポテンシャルを持っています。このチュートリアルプロジェクトでは、古典的なじゃんけんのゲームを構築します。

総評

プロジェクト1と同じく、このインストラクターもこのチュートリアルをうまくこなしています。彼にはエンターテイメント性があり、十分に付いて行ける様な話し方をしています。このプロジェクトもまたバニラJavaScriptをベースにしています。

星4/5

9. まるばつゲームを作る

Youtubeリンク

構築するもの

このプロジェクトでは、楽しいだけでなく複雑なエクササイズである基本的なAIとアルゴリズムを使用した、まるばつゲームを構築します。ですが、それ以外はすべてバニラJS、HTML、CSSで作られています。

総評

このプロジェクトのサイズ感はよく、構造も明確で楽しく構築していくことができます。インストラクターの声ははっきりしており、説明にもついて行きやすいです。説明もとても上手く、最後はとても気分が良くなるはずです。

星5/5

おまけ: 9-in-1ミニプロジェクトを作る

Youtubeリンク

構築するもの

最後に紹介させてもらいたいのがこちら。一つの動画でJavaScript、HTML、CSSを練習する9つの小さな独立したプロジェクトを続けて構築していくチュートリアルです。もしかしたら、これらのうちのどれかをスニペットとして自身のサイトで再利用できるかもしれませんね。

総評

1つの動画に複数の小さな断片を入れるというアイデアが気に入りました。インストラクターの声はもう少しはっきりしていた方が好ましく、所々聞き取りづらい場面もありました。

星3/5

ということで以上になります。これらの優れたチュートリアルを始めるのが待ちきれないことでしょう。ですが一つだけ言わせてください。動画を見て、それに合わせてコーディングすることは練習するための良い方法ですが、独自のフレーバーを加えること、つまり機能を強化、アレンジしたり、組み合わせて改善などをすることで、最大限の効果を得ることができます。また、自分自身でプロジェクトを作りその工程を文書化できればなお良いでしょう。そうすれば、そこから多くのことが学べるだけでなく、いろんな人がきっとあなたに感謝することでしょう!

翻訳協力

Original Author: Simon Holdorf
Thank you for letting us share your knowledge!

この記事は以下の方々のご協力により公開する事が出来ました。
改めて感謝致します。
選定担当: yumika tomita
翻訳担当: shiro1
監査担当: takujio
公開担当:@aoharu

私たちと一緒に記事を作りませんか?

私たちは、海外の良質な記事を複数の優秀なエンジニアの方の協力を経て、日本語に翻訳し記事を公開しています。
活動に共感していただける方、良質な記事を多くの方に広めることに興味のある方は、ぜひご連絡ください。
Mailでタイトルを「参加希望」としたうえでメッセージをいただく、もしくはTwitterでメッセージをいただければ、選考のちお手伝いして頂ける部分についてご紹介させていただく事が可能です。
※ 頂いたメッセージには必ずご返信させて頂きます。

ご意見・ご感想をお待ちしております

今回の記事は、いかがだったでしょうか?
・こうしたら良かった、もっとこうして欲しい、こうした方が良いのではないか
・こういったところが良かった
などなど、率直なご意見を募集しております。
いただいたお声は、今後の記事の質向上に役立たせていただきますので、お気軽にコメント欄にてご投稿ください。Twitterでもご意見を受け付けております。
みなさまのメッセージをお待ちしております。

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

JavaScriptで完璧なディープコピーをしようと頑張った

javascriptで完璧にオブジェクトを複製したい!

JavaScriptでオブジェクトをコピーしようとした時、シャローコピーならスプレッド構文でなんとかなるんですが、ディープコピーしようとすると面倒だったのでいい方法を探したのですが、あまりいいものが見つからなかったので関数を作りました。
頑張ったので褒めてください。
jQueryのextendsとやらでできるらしいですが、このためだけにjQueryを入れたくはないですしおすし。
Qiitaの記事を書くのは初めてなので、至らぬ点があるかもしれませんがどうかご容赦を。

ディープコピーって何?

Javascriptのオブジェクトは参照型です。
プロパティーが変更されると、そのオブジェクトを参照している変数がすべて影響を受けます。
そのため、元のオブジェクトのプロパティーを変更したくない場合、完全に別オブジェクトとして複製する必要があります。
その中で、一番浅い階層のみを複製するのがシャローコピー(Shallow Copy)、すべての階層を複製するのがディープコピー(Deep Copy)です。

const hoge = {
  foo: "吾輩は猫である",
  baz: {
    abc: "名前はまだ無い"
  }
};
const huga = hoge;
const piyo = {...huga}; // <= スプレッド構文(シャローコピー)

huga.foo = "ねこです。よろしくおねがいします。";

hoge.foo // => "ねこです。よろしくおねがいします。" <= 参照元も影響を受ける
huga.foo // => "ねこです。よろしくおねがいします。"
piyo.foo // => "吾輩は猫である" <= きちんとコピーできている

hoge === huga // => true
hoge === piyo // => false

huga.baz.abc = "ねこはいます。";

hoge.baz.abc // => "ねこはいます。"
huga.baz.abc // => "ねこはいます。"
piyo.baz.abc // => "ねこはいます。" <= 上書きされてしまっている

hoge.baz === huga.baz // => true
hoge.baz === piyo.baz // => true

JSON.parse(JSON.stringify())

一旦JSON(文字列)に変換してからJSのオブジェクトに戻す方法です。
おそらく一番簡単なディープコピー方法ですが、undefined, Symbol, Getter/Setter等JSONで表現できないものはコピーできません。

const 複製したオブジェクト = JSON.parse(JSON.stringify(コピー元オブジェクト));

Object.create()

prototypeを指定してオブジェクトを作成できるやつです。
元オブジェクトをprototypeにしてオブジェクトを作るということですね。
DevToolなどで見るとプロパティーがありませんが、きちんとprototypeからアクセスできます。
コピー先を変更しても、コピー元オブジェクトはも変わりません。
が、コピー元オブジェクトが変更されるとコピー先も変更を受けます。
コピー元がイミュータブルである場合には使えるかもしれません。

const 複製したオブジェクト = Object.create(コピー元オブジェクト);

作る

いい感じにコピーできる手段が見つからなかったので、作りましょう。
最初からごちゃごちゃさせても分かりづらいので、とりあえず単純に。(単純じゃない気がする)
プロパティーのキーを取得してreduceで値を追加していきます。
Object.keysだと一部プロパティーが取得できないのでObject.getOwnPropertyNamesObject.getOwnPropertySymbolsを使用します。
reduceの第二引数(初期値)は、prototypeだけ継承したオブジェクトを指定します。

const clone = (object)=>{
  if(typeof object !== "object")return object;
  const propNames = Object.getOwnPropertyNames(object);
  const symbols = Object.getOwnPropertySymbols(object);
  const prototype = Object.getPrototypeOf(object);
  return [...propNames, ...symbols].reduce((propertiesObject, propName)=>{
    // プロパティーの取得
    const prop = Object.getOwnPropertyDescriptor(object, propName);
    // メソッドでなければ再起呼出し
    if(prop.hasOwnProperty("value"))
      prop.value = clone(prop.value);
    // プロパティーを追加
    Object.defineProperty(propertiesObject, propName, prop);
    return propertiesObject;
  }, Object.create(prototype));
};

配列や他のオブジェクトにも対応させましょう。
typeof演算子ではオブジェクトは全て"object"になってしまうので、Object.prototype.toStringを使った関数を定義しています。
プリミティブ型のオブジェクトはオブジェクトのまま返していますが、単純なプリミティブ型を帰す場合はreturn new...を消してください。
DateやMap等は同じように複製できないので個別に処理をしています。

const typeOf = ()=>Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();
const clone = (object)=>{
    if(Array.isArray(object)) // [], new Array
        return object.map(value=>clone(value));
    if(typeof object === "object")switch(typeOf(object)){
    default: // new Foo etc...
    case "object": { // {}, new Object
        // ほぼ同じなので省略
    }
    case "number": // new Number
        return new Number(object);
    case "string": // new String
        return new String(object);
    case "boolean": // new Boolean
        return new Boolean(object);
    case "bigint": // Object(BigInt())
        return object.valueOf();
    case "regexp": // /regexp/, new RegExp
        return new RegExp(object);
    case "null": // null
        return null;
    case "date": return new Date(object);
    case "map": {
        const map = new Map();
        for(const [key, value] of object)
            map.set(key, clone(value));
        return map;
    }
    case "weakmap": {
        const map = new WeakMap();
        for(const [key, value] of object)
            map.set(key, clone(value));
        return map;
    }
    case "set": return new Set(object);
    case "weakset": return new WeakSet(object);
    }
    // primitive type, function
    return object;
};

完成?

こんな感じになりました。
existingObjectsにこれまでcloneしたオブジェクトを入れて再帰参照があったらエラー吐いてます。
ただ、これでも一部のオブジェクトは完璧にはコピーできないのですがね。
クロージャーで作成したような隠しパラメータを持っているようなのがだめっぽい気がします。
完璧にはテストできてないので不具合とかあるかもしれない。

const typeOf = ()=>Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();
const clone = (object, existingObjects)=>{
    if(!existingObjects)existingObjects = [];
    else if(existingObjects.indexOf(object) !== -1)
        throw new Error("Recursive reference exists.");
    else existingObjects = [...existingObjects, object];
    if(Array.isArray(object)) // [], new Array
        return object.map(value=>clone(value, existingObjects));
    if(typeof object === "object")switch(typeOf(object)){
    default: // new Foo etc...
    case "object": { // {}, new Object
        const symbols = Object.getOwnPropertySymbols(object);
        const propNames = Object.getOwnPropertyNames(object);
        const prototype = Object.getPrototypeOf(object);
        return [...propNames, ...symbols].reduce((propertiesObject, propName)=>{
            const prop = Object.getOwnPropertyDescriptor(object, propName);
            if(prop.hasOwnProperty("value"))
                prop.value = clone(prop.value, existingObjects);
            Object.defineProperty(propertiesObject, propName, prop);
            return propertiesObject;
        }, Object.create(prototype));
    }
    case "number": // new Number
        return new Number(object);
    case "string": // new String
        return new String(object);
    case "boolean": // new Boolean
        return new Boolean(object);
    case "bigint": // Object(BigInt())
        return object.valueOf();
    case "regexp": // /regexp/, new RegExp
        return new RegExp(object);
    case "null": // null
        return null;
    case "date": return new Date(object);
    case "map": {
        const map = new Map();
        for(const [key, value] of object)
            map.set(key, clone(value, existingObjects));
        return map;
    }
    case "weakmap": {
        const map = new WeakMap();
        for(const [key, value] of object)
            map.set(key, clone(value, existingObjects));
        return map;
    }
    case "set": return new Set(object);
    case "weakset": return new WeakSet(object);
    }
    // primitive type, function
    return object;
}

ヲマケ(シャローコピー)

簡単なシャローコピーならスプレッド構文やObject.assign()で実現できます。
ただしこの方法は、Getter/Setterがコピーできません。

const hoge = {
  foo: "いろはにほへと ちりぬるを",
  baz: "わかよたれそ つねならむ",
  bar: "うゐのおくやま けふこえて",
  qux: "あさきゆめみし ゑひもせす"
};
const huga = {...hoge};
const piyo = Object.assign({}, hoge);
hoge === huga // => false
hoge === piyo // => false

Getter/Setterもコピーしたければディープコピーの以下の部分とcloneの再帰呼び出しを消して使用してください。

if(!existingObjects)existingObjects = [];
else if(existingObjects.indexOf(object) !== -1)
  throw new Error("Recursive call exists.");
else existingObjects = [...existingObjects, object];

参考

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

パターン指定型パスワード ジェネレーター (JS版)

JavaScript を思い出す & 訓練すべく作ってみる。

IE8 でも動作するようにしたら、かなりアレな感じになった。

HTMLファイル
ppwgen.html
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" >
    <title>パターン指定型パスワード生成</title>
  </head>
  <body onload="cpwSample();">
    <h1>パターン指定型パスワード生成</h1>
    <hr>
    <p>
      <input type="text" id="cpwinp1" size="50" placeholder="ここに生成パターンを入力" style="font-size: large;" /><br/>
      <input id="cpwgen" type="button" value="クリックすると生成します" onclick="cpwStart('1');" />
    </p>
    <p>パスワード(JavaScript によるローカル生成です):</p>
    <blockquote><pre id="cpwout1">なし</pre></blockquote>
    <hr>
    <p>文字の選択は、'%' の後に</p>
    <blockquote>
      <div>% 種類 </div>
      <div>% 文字数 種類 </div>
    </blockquote>
    <p>の形式で指定します。文字数を省略すると1文字になります。</p>
    <blockquote>
      <table border="1" style="border-collapse: collapse; border-spacing: 0px;">
        <tr><th>&nbsp;種類&nbsp;</th><th>内容</th><th>文字</th></tr>
        <tr><td style="text-align: center;">b</td><td>&nbsp; 2進数の数字&nbsp;</td><td>&nbsp;01&nbsp;</td></tr>
        <tr><td style="text-align: center;">o</td><td>&nbsp; 8進数の数字&nbsp;</td><td>&nbsp;01234567&nbsp;</td></tr>
        <tr><td style="text-align: center;">d</td><td>&nbsp;10進数の数字&nbsp;</td><td>&nbsp;0123456789&nbsp;</td></tr>
        <tr><td style="text-align: center;">X</td><td>&nbsp;16進数の数字(大)&nbsp;</td><td>&nbsp;0123456789ABCDEF&nbsp;</td></tr>
        <tr><td style="text-align: center;">x</td><td>&nbsp;16進数の数字(小)&nbsp;</td><td>&nbsp;0123456789abcdef&nbsp;</td></tr>
        <tr><td style="text-align: center;">A</td><td>&nbsp;英文字&nbsp;</td><td>&nbsp;ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz&nbsp;</td></tr>
        <tr><td style="text-align: center;">C</td><td>&nbsp;英文字(大)&nbsp;</td><td>&nbsp;ABCDEFGHIJKLMNOPQRSTUVWXYZ&nbsp;</td></tr>
        <tr><td style="text-align: center;">c</td><td>&nbsp;英文字(小)&nbsp;</td><td>&nbsp;abcdefghijklmnopqrstuvwxyz&nbsp;</td></tr>
        <tr><td style="text-align: center;">B</td><td>&nbsp;英文字の母音&nbsp;</td><td>&nbsp;AEIOUaeiou&nbsp;</td></tr>
        <tr><td style="text-align: center;">V</td><td>&nbsp;英文字の母音(大)&nbsp;</td><td>&nbsp;AEIOU&nbsp;</td></tr>
        <tr><td style="text-align: center;">v</td><td>&nbsp;英文字の母音(小)&nbsp;</td><td>&nbsp;aeiou&nbsp;</td></tr>
        <tr><td style="text-align: center;">D</td><td>&nbsp;英文字の子音&nbsp;</td><td>&nbsp;BCDFGHJKLMNPQRSTVWXYZbcdfghjklmnpqrstvwxyz&nbsp;</td></tr>
        <tr><td style="text-align: center;">Q</td><td>&nbsp;英文字の子音(大)&nbsp;</td><td>&nbsp;BCDFGHJKLMNPQRSTVWXYZ&nbsp;</td></tr>
        <tr><td style="text-align: center;">q</td><td>&nbsp;英文字の子音(小)&nbsp;</td><td>&nbsp;bcdfghjklmnpqrstvwxyz&nbsp;</td></tr>
        <tr><td style="text-align: center;">Y</td><td>&nbsp;英数&nbsp;</td><td>&nbsp;0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz&nbsp;</td></tr>
        <tr><td style="text-align: center;">Z</td><td>&nbsp;英数(大)&nbsp;</td><td>&nbsp;0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ&nbsp;</td></tr>
        <tr><td style="text-align: center;">z</td><td>&nbsp;英数(小)&nbsp;</td><td>&nbsp;0123456789abcdefghijklmnopqrstuvwxyz&nbsp;</td></tr>
        <tr><td style="text-align: center;">W</td><td>&nbsp;英数と&nbsp;'_'&nbsp;</td><td>&nbsp;0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_&nbsp;</td></tr>
        <tr><td style="text-align: center;">L</td><td>&nbsp;英数(大)と&nbsp;'_'&nbsp;</td><td>&nbsp;0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_&nbsp;</td></tr>
        <tr><td style="text-align: center;">l</td><td>&nbsp;英数(小)と&nbsp;'_'&nbsp;</td><td>&nbsp;0123456789abcdefghijklmnopqrstuvwxyz_&nbsp;</td></tr>
      </table>
    </blockquote>
    <p>複数の連続指定</p>
    <blockquote><div>%2c%3x%4d</div></blockquote>
    <p></p>
    <blockquote><div id="cpwinp2">%{2c3x4d}</div></blockquote>
    <p>として、{..} で簡略化できます。</p>
    <blockquote><pre id="cpwout2" style="color: gray;"></pre></blockquote>
    <p>さらに</p>
    <blockquote><div id="cpwinp3">%3{2c3x4d}</div></blockquote>
    <p>とすると、{..} を3回の繰り返しにできます。</p>
    <blockquote><pre id="cpwout3" style="color: gray;"></pre></blockquote>
    <p>省略形の途中に文字を入れるには、バックスラッシュ "\" を使って</p>
    <blockquote><div id="cpwinp4">%{2c\-3x\-4d}</div></blockquote>
    <p>とします。(バックスラッシュなしでも可能な場合があります)</p>
    <blockquote><pre id="cpwout4" style="color: gray;"></pre></blockquote>
    <p>選択する文字を限定するには [..] を使って</p>
    <blockquote><div id="cpwinp5">%20[ABHKO]</div></blockquote>
    <p>とします。例では ABHKO のどれかが出てきます。</p>
    <blockquote><pre id="cpwout5" style="color: gray;"></pre></blockquote>
    <p>選択文字を [..] の中に連続した文字の順番で並べる場合</p>
    <blockquote><div>%10[ABCDEF]</div></blockquote>
    <p><p>
      <blockquote><div id="cpwinp6">%10[A-F]</div></blockquote>
    <p>として、[ 開始 - 終了 ] で簡略化できます。</p>
    <blockquote><pre id="cpwout6" style="color: gray;"></pre></blockquote>
    <hr>
    <p>サンプル</p>


    <p><span>書式を列挙</span><span>&nbsp;:&nbsp;</span>
      " <span id="cpwinp11">%b %o %d %X %x %A %C %c %B %V %v %D %Q %q %Y %Z %z %W %L %l</span> "
    </p><blockquote><pre id="cpwout11"></pre></blockquote>

    <p><span>書式を列挙(空白なし)</span><span>&nbsp;:&nbsp;</span>
      " <span id="cpwinp12">%b%o%d%X%x%A%C%c%B%V%v%D%Q%q%Y%Z%z%W%L%l</span> "
    </p><blockquote><pre id="cpwout12"></pre></blockquote>

    <p><span>書式を列挙('{..}' による省略形)</span><span>&nbsp;:&nbsp;</span>
      " <span id="cpwinp13">%{bodXxACcBVvDQqYZzWLl}</span> "
    </p><blockquote><pre id="cpwout13"></pre></blockquote>

    <p><span>10桁の数字</span><span>&nbsp;:&nbsp;</span>
      " <span id="cpwinp14">%10d</span> "
    </p><blockquote><pre id="cpwout14"></pre></blockquote>

    <p><span>ライセンス キー風</span><span>&nbsp;:&nbsp;</span>
      " <span id="cpwinp15">LK%5{\-C2Z3d}</span> "
    </p><blockquote><pre id="cpwout15"></pre></blockquote>

    <hr>
    <script type="text/javascript">
      function cpwSample()
      {
          for (var n = 2; n <= 20; n++)
          {
              var inp = document.getElementById("cpwinp"+n);
              var out = document.getElementById("cpwout"+n);
              if (inp && out) cpwMake(out, inp.innerText, 3);
          }
      }
    </script>
    <script type="text/javascript">
    <!--

      var cpwPatternMap = cpwGetPatternMap();

      function newScriptingDictionary()
      {
          try { return new ActiveXObject("Scripting.Dictionary"); } catch (e) {}
          try { return new cpwScriptingDictionary(); } catch (e) {}
      }

      function cpwScriptingDictionary()
      {
          this.map = new Map();
          this.add = function(k, v) { this.map.set(k, v); }
          this.Exists = function(k) { return this.map.has(k); }
          this.Item = function(k) { return this.map.get(k); }
      }

      function cpwGetPatternMap()
      {
          var dict = newScriptingDictionary();
          dict.add('b', '01');
          dict.add('o', '01234567');
          dict.add('d', '0123456789');
          dict.add('X', '0123456789ABCDEF');
          dict.add('x', '0123456789abcdef');
          dict.add('A', 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz');
          dict.add('C', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ');
          dict.add('c', 'abcdefghijklmnopqrstuvwxyz');
          dict.add('B', 'AEIOUaeiou');
          dict.add('V', 'AEIOU');
          dict.add('v', 'aeiou');
          dict.add('D', 'BCDFGHJKLMNPQRSTVWXYZbcdfghjklmnpqrstvwxyz');
          dict.add('Q', 'BCDFGHJKLMNPQRSTVWXYZ');
          dict.add('q', 'bcdfghjklmnpqrstvwxyz');
          dict.add('Y', '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz');
          dict.add('Z', '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ');
          dict.add('z', '0123456789abcdefghijklmnopqrstuvwxyz');
          dict.add('W', '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_');
          dict.add('L', '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_');
          dict.add('l', '0123456789abcdefghijklmnopqrstuvwxyz_');
          return dict;
      }

      function cpwMake(out, pattern, count)
      {
          var res = cpwGenerator(pattern, count);
          while (out.firstChild)
              out.removeChild(out.firstChild);
          out.appendChild(res);
      }

      function cpwStart(suffix)
      {
          var inp = document.getElementById("cpwinp" + suffix);
          var out = document.getElementById("cpwout" + suffix);
          cpwMake(out, inp.value, 10);

          cpwSample();
      }

      function cpwRandom()
      {
          this.choice = function(tab)
          {
              return tab.charAt(Math.floor(Math.random() * tab.length));
          }
      }

      function cpwGenCharSet(charset)
      {
          this.charset = charset;
          this.generate = function(rnd)
          {
              return rnd.choice(this.charset);
          }
      }

      function cpwGenCount(count, generator)
      {
          this.generator = generator;
          this.count = count;
          this.generate = function(rnd)
          {
              var r = '';
              for (var n = 0; n < this.count; n++)
                  r += this.generator.generate(rnd);
              return r;
          }
      }

      function cpwGenList(generators)
      {
          this.generators = generators;
          this.generate = function(rnd)
          {
              var r = '';
              for (var n in this.generators)
                  r += this.generators[n].generate(rnd);
              return r;
          }
      }

      function cpwPatternCharList(pattern)
      {
          var clist = new Array();
          var p = '';

          for (var cp = 0; cp < pattern.length; cp++)
          {
              var c = pattern.charAt(cp);

              if (p != '\\')
              {
                  clist.push(c);
                  p = c;
              }
              else
              {
                  clist.push(clist.pop() + c);
                  p = '';
              }
          }
          return clist;
      }

      function cpwPatternIterator(pattern)
      {
          this.clist = cpwPatternCharList(pattern);
          this.pointer = 0;
          this.done = (this.pointer >= this.clist.length);

          this.next = function()
          {
              var c = '';
              if (!this.done)
              {
                  c = this.clist[this.pointer++];
                  this.done = (this.pointer >= this.clist.length);
              }
              return c;
          };

          this.rewind = function()
          {
              --this.pointer;
              return this;
          }
      }

      function cpwUnescape(s)
      {
          return ((s.length > 1 && s[0] == '\\') ? s[1] : s);
      }

      function cpwParsePat(ptr, nest)
      {
          var cnt = 0;
          var len = -1
          var c = '';

          while (!ptr.done)
          {
              c = ptr.next();
              if (!('0' <= c && c <= '9')) break;
              cnt = (cnt * 10) + parseInt(c, 10);
              len = cnt;
          }
          if (len < 0) len = 1;

          switch (c)
          {
              case '': throw undefined;
              case '{': return new cpwGenCount(len, cpwParseSubPat(ptr));
              case '[': return new cpwGenCount(len, cpwParseSubSet(ptr));
              default: break;
          }
          if (cpwPatternMap.Exists(c))
              return new cpwGenCount(len, new cpwGenCharSet(cpwPatternMap.Item(c)));
          if (nest)
          {
              if (c.length > 1 && c[0] == '\\')
                  return new cpwGenCharSet(cpwUnescape(c));

              var cu = c.toUpperCase();
              if (c != '}' && !('A' <= cu && cu <= 'Z'))
                  return new cpwGenCharSet(c);
          }
          throw undefined;
      }

      function cpwParseSubPat(ptr)
      {
          var r = new Array();

          while (!ptr.done)
          {
              var c = ptr.next();
              if (c == '}') break;
              r.push(cpwParsePat(ptr.rewind(), true));
          }
          return new cpwGenList(r);
      }

      function cpwParseSubSet(ptr)
      {
          var r = new Array();
          var s = new Array();
          while (!ptr.done)
          {
              var c = ptr.next();
              if (c == ']') break;
              if (s.length < 2)
              {
                  s.push(c);
                  continue;
              }
              a = cpwUnescape(s[0]);
              if (s[1] != '-')
              {
                  r.push(a);
                  s.shift();
                  s.push(c);
                  continue;
              }
              b = cpwUnescape(c);

              var cs = a.charCodeAt(0);
              var ce = b.charCodeAt(0);
              for (var c = cs; c <= ce; c++)
                  r.push(String.fromCharCode(c));
              s = new Array();
          }
          p = '';
          for (var n in r) p += r[n];
          for (var n in s) p += s[n];
          return new cpwGenCharSet(p);
      }

      function cpwParse(pattern)
      {
          var ptr = new cpwPatternIterator(pattern);
          var r = new Array();

          while (!ptr.done)
          {
              var c = ptr.next();
              if (c != '%')
                  g = new cpwGenCharSet(c);
              else
                  g = cpwParsePat(ptr, false);
              r.push(g);
          }
          return new cpwGenList(r);
      }

      function cpwGenerator(pattern, count)
      {
          var parent = document.createElement('div');

          try
          {
              var g = cpwParse(pattern);
              var rnd = new cpwRandom();

              for (var i = 0; i < count; i++)
              {
                  var el = document.createElement('div');
                  el.innerText = g.generate(rnd);
                  parent.appendChild(el);
              }
          }
          catch (e)
          {
              var el = document.createElement('div');
              el.innerText = 'Error: invalid pattern: ' + pattern;
              parent.appendChild(el);
          }

          return parent;
      }

      // -->
    </script>
  </body>
</html>

HTML を開くと、こんな感じ。

スクリーン ショット

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

コロナウイルス対策でテレワークに移行するべきだと思っていてもちゃんと仕事をするか不安だったり、なんとなく抵抗がある人達を安心させる為に報告を催促するLINEボットを作った

はじめに

コロナウイルス早く終息して欲しい!
元同僚に会社がテレワークにまだ移行しておらず、テレワークで仕事は可能なのに出勤しなければいけないと聞いたので、少しでも上司に安心してもらって移行してもらえるように、そして感染拡大を少しでも防ぐ為に今ハマってるIFROを使って、流行り?の分報ならぬ時報ボットを作ってみようと思います!

そして、コストがかかるのも抵抗があると思うので(LINEのプッシュ料金)は気にはなるものの、実際に報告書を作るプログラムはGoogleAppsScriptを使っていこうと思います。

今回作ったもの

IFROテンプレートストア
GoogleAppsScript
LINEのボット(サンプル)
428cplwq.png
※このサンプルは無料のアカウントなのでプッシュ上限になったら報告書の送信と催促の通知は送られません。。。

ボットの役割(ざっくり)

  • 報告を取りまとめる。
  • 前回の報告から一定の時間経ったら報告を催促する。
  • 仕事が終わったら報告書を上司に送る。

ボットが作ってくれる成果物

こんな感じのGoogle Documentの業務日報

スクリーンショット 2020-03-31 12.30.38.png

LINE

まずは報告用のボットのアカウントを作っていきます。
これは特に難しいことはないですね。過去の記事プログラミング無しでのLINE BOTの作り方でやったようにアカウントを作っていきます。

スクリーンショット 2020-03-28 10.19.35.png

チャネル名や説明は適当に。

スクリーンショット 2020-03-29 02.53.26.png

今回は挨拶文もちょっと変えておきます。
最初の発話でFlex Messageを送ったり出来ないので、一応発話コマンドを載せておくことにします。

IFRO①

プロジェクトの作成

スクリーンショット 2020-03-28 9.39.12.png

「+新規スキル」をクリックして新規プロジェクトを作ります。

スクリーンショット 2020-03-28 15.04.34.png

プロパティを開いてスキル名を付けます。

スクリーンショット 2020-03-29 01.44.54.png

続いて、今のところ思いつくプロパティを設定します。
プロパティには、あとから簡単に変更したい値や繰り返し発話する発話内容などを設定しておくと便利です。

  • 催促の間隔 : 最後のユーザーアクションから何分後に催促するか
  • 報告を送る上司のLINE uid : 報告を上司に送る為に上司のIDが必要ですので、それを設定するプロパティです。
・ 業務開始 > 報告書の作成を始める。
・ 休憩開始 > 催促を止める。
・ 休憩終了 > 催促を再開する。
・ 業務終了 > 報告書を上司に提出する。
・ 上記以外の発話(報告) > 報告書に書く。

LINEの挨拶文にも書いた通り、このような感じでBotに反応させたいので構成は下記のような感じですかね。

① 聞き取り
  ↓
② 記憶保存
  ↓
③ 記憶出力
  ↓
④ 分 岐
  ↓
⑤
  (業務開始してない)業務開始 > 報告書の雛形をコピーして名前をつける。
  (業務開始している且つ休憩中じゃない)休憩開始 > 催促用のトリガーを止める。
  (業務開始している且つ休憩中)休憩終了 > 催促用のトリガーをスタートする。
  (業務開始している)業務終了 > 報告書をPDFで保存して上司にPushで送る。
  (業務開始している)上記以外の発話(報告) > 報告書の雛形に時間と共に書き込む
  業務開始してなかったら業務開始ボタンを表示する。
  ↓
⑥ 何かしら発話しつつ、報告以外の発話のボタンを表示する。
  ↓
⑦ ①へ戻る

それでは、早速作っていきたいと思います

スクリーンショット 2020-03-29 02.55.18.png
一番上の階層はこんな感じ。
「発話内容」という記憶を作り、発話内容を保存出来るようにします。
結構ボックスが多くなりそうなので、グループモジュールを使って見た目をシンプルにしていきます。

スクリーンショット 2020-03-30 17.31.48.png

「業務開始」と発話された場合のグループです。
業務中かどうかを判定する記憶が必要なので、プロパティとうまく組み合わせます。
業務を開始したらプロパティ出力モジュールを使って「業務中フラグ」プロパティを出力し、「業務フラグ」という記憶に保存します。
また、休憩中かどうかも判定し、状態にあった対応を取れるように「休憩中フラグ」も使っていきます。
メインとなるWebhookモジュールとメッセージオブジェクト出力モジュール(LINE)はあとからいれることにします。

スクリーンショット 2020-03-30 17.34.10.png

初回だけ名前と部署を聞くようにします。
初期値だったら質問を発話、初期値以外だったら次の質問へ・・・といった感じです。

スクリーンショット 2020-03-29 03.15.21.png
「休憩開始」と発話された場合のグループです。
業務開始と同様、業務フラグを判定し処理を分けていきます。
ちなみに分岐から先は、左から1(業務中)、2(業務終了)、3(休憩中)って感じに統一してボックスを置いていってます。

スクリーンショット 2020-03-29 03.39.13.png
続いて、「休憩終了」のグループ。
大体似たような構造です。

スクリーンショット 2020-03-29 03.43.40.png
こちらは「業務終了」と発話された場合のグループです。
とりあえず今のところ説明は不要ですね。

スクリーンショット 2020-03-29 03.47.32.png
最後、「報告」のグループです。

メッセージオブジェクト出力モジュール(LINE)の作成

報告以外のコマンドは、発話でもいいけどボタンでも出来るようにしておきたいのでFlex Messageを使ってBot側の発話を作っていきます。

Flex Message Simulatorを使えば簡単に作れるので、ササッと作っちゃいましょう。
凝ったものを作ろうと思えばいくらでも出来るっぽいのですが、今回はあまり重視しなくて良いと思うのでシンプルに作っていきます。

スクリーンショット 2020-03-29 04.09.11.png
こんな感じでいいかなと思います。

右上の「View as JSON」を押すと下の画像にような感じになるのでCopyを押します。

スクリーンショット 2020-03-29 04.10.43.png

このコピーしたJSONをメッセージオブジェクト出力モジュール(LINE)の中に貼り付けて「ここに発話など文章」に部分を適宜文章を変えていこうと思います。
それぞれのケースに合った文章にすれば良いと思うので全て載せるのは割愛しますが、この部分をプロパティにしておくことでプロパティの設定部分からあとから文章を簡単に変えるようにすることも出来ます。
また、ケースによって表示不要なボタンもあると思うので、外してあげるとUX的に良い感じにもなる気がします。

スクリーンショット 2020-03-29 04.16.14.png
こういう感じで全てのメッセージオブジェクト出力モジュール(LINE)を埋めていきます。

この作業が終われば、設定してないボックスはWebhookモジュールのみになりますが、先にGoogleAppsScriptを書いていこうと思います。
Webhookモジュールの設定は#IFRO②で書きます。

GoogleAppsScript

実装する機能としては下記の6つになると思いますので、順番に書いていこうと思います。

1 報告書を作成する。
2 休憩開始。
3 休憩終了。
4 報告を書く。
5 報告書を上司に提出する。

コピー用スクリプトソースコードのリンクを置いておきます。
※当方、素人なので割とごちゃごちゃしています。すみません。
コピーじゃない場合は、Momentを使っているので、ライブラリにMHMchiX6c1bwSqGM1PZiW_PxhMjh3Sh48を追加します。

main.gs
function doPost(e){
  var pid = JSON.parse(e.parameter.parameter1).pid;
  var department = decodeURI(JSON.parse(e.parameter.parameter1).department);
  var name = decodeURI(JSON.parse(e.parameter.parameter1).name);
  var directory_id = decodeURI(JSON.parse(e.parameter.parameter1).directory_id);
  setProp("add_minutes", JSON.parse(e.parameter.parameter2).add_minutes);

  switch (pid){
    case 1:
      createDocument(name, department, directory_id);
      addLine(name, department, directory_id, "業務開始");
      setTrigger(decodeURI(JSON.parse(e.parameter.parameter2).usr_uid), getProp("add_minutes"));
      break;
    case 2:
      addLine(name, department, directory_id, "休憩開始");
      delTrigger(decodeURI(JSON.parse(e.parameter.parameter2).usr_uid), getProp("add_minutes"));
      break;
    case 3:
      addLine(name, department, directory_id, "休憩終了");
      setTrigger(decodeURI(JSON.parse(e.parameter.parameter2).usr_uid), getProp("add_minutes"));
      break;
    case 4:
      addLine(name, department, directory_id, decodeURI(JSON.parse(e.parameter.parameter2).report_content));
      setTrigger(decodeURI(JSON.parse(e.parameter.parameter2).usr_uid), getProp("add_minutes"));
      break;
    case 5:
      addLine(name, department, directory_id, "業務終了");
      setProp("access_token", decodeURI(JSON.parse(e.parameter.parameter2).access_token));
      sendReport(name, department, directory_id, decodeURI(JSON.parse(e.parameter.parameter2).mng_uid), getProp("access_token"));
      delTrigger(decodeURI(JSON.parse(e.parameter.parameter2).mng_uid), getProp("add_minutes"));
    default:
      break;
  }
}

1〜4 報告書を作成する〜報告まで

1〜4までは、ほぼ同じなのでまとめて書きます。

    case 1:
      createDocument(name, department, directory_id);
      addLine(name, department, directory_id, "業務開始");
      setTrigger(decodeURI(JSON.parse(e.parameter.parameter2).usr_uid), getProp("add_minutes"));
      break;

createDocumentで日報用のGoogleDocumentを作成していきます。

control_document.gsより
function createDocument(name, department, directory_id){
  //日報の名前を付ける
  var d = Moment.moment().format("YYYY年M月D日");
  var fName = d + department + name;

  //ドキュメントを格納するフォルダを取得or作成
  var targetFolder = getFolder(directory_id, Moment.moment());
  //フォルダにドキュメントが存在するか確認
  var docExists = getFileId(targetFolder, Moment.moment().format("YYYY年M月D日") + department + name);
  //なければドキュメントを作成
  if (!docExists){

    //ドキュメントの作成
    var doc = DocumentApp.create(fName);
    var docBody = doc.getBody();

    /*ドキュメントの初期設定*/
    //タイトル
    var p_title = docBody.appendParagraph("業務日報\n");
    p_title.setFontSize(18);
    p_title.setAlignment(DocumentApp.HorizontalAlignment.CENTER);
    //日付
    var p_day = docBody.appendParagraph(d);
    p_day.setFontSize(11);
    p_day.setAlignment(DocumentApp.HorizontalAlignment.RIGHT);
    //部署と名前
    var p_department = docBody.appendParagraph("部 署:" + department);
    p_department.setAlignment(DocumentApp.HorizontalAlignment.RIGHT);
    var p_name = docBody.appendParagraph("氏 名:" + name + "\n");
    p_name.setAlignment(DocumentApp.HorizontalAlignment.RIGHT);

    //セーブ
    doc.saveAndClose();

    //格納するフォルダへ移動
    var docFile = DriveApp.getFileById(doc.getId());
    targetFolder.addFile(docFile);

    //ルート直下のファイルを消す
    DriveApp.removeFile(docFile);
  }
}
common.gsより
function getFolder(directory_id, d){
  //フォルダオブジェクトを取得
  var parentFolder = DriveApp.getFolderById(directory_id);

  //親フォルダのフォルダイテレータリストを取得
  var iteratorList = parentFolder.getFolders();

  //保存するフォルダを設定、すでにあればオブジェクト取得、無ければ作成してオブジェクト取得
  var saveFolder;
  var saveFoldersName = 'DailyReports';
  var folderExists = false;

  while (iteratorList.hasNext()){
    saveFolder = iteratorList.next();
    if(saveFolder.getName() == saveFoldersName){
      folderExists = true;
      break;
    }
  }
  if (!folderExists){
    saveFolder = parentFolder.createFolder(saveFoldersName);
  }

  //保存フォルダのフォルダイテレータリストを取得
  iteratorList = saveFolder.getFolders();

  //今日のフォルダを設定、すでにあればオブジェクト取得、無ければ作成してオブジェクト取得
  var todayFolder;
  var todayFoldersName = d.format("YYYY-MM-DD");
  folderExists = false;

  while (iteratorList.hasNext()){
    todayFolder = iteratorList.next();
    if(todayFolder.getName() == todayFoldersName){
      folderExists = true;
      break;
    }
  }
  if (!folderExists){
    todayFolder = saveFolder.createFolder(todayFoldersName);
    todayFolder.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
  }
  return todayFolder;
}
common.gsより
function getFileId(folder, title){
  //親フォルダのファイルイテレータリストを取得
  var iteratorList = folder.getFiles();

  //オブジェクトを検索
  var docFile;
  var fileExists = false;

  while (iteratorList.hasNext()){
    docFile = iteratorList.next();
    if(docFile.getName() == title){
      fileExists = true;
      break;
    }
  }
  if (!fileExists){
    return false;
  }
  return docFile.getId();
}

ドキュメントを作成したら、addLINEを呼び出して、ドキュメントに「業務開始」と書き込みます。

control_document.gsより
function addLine(name, department, directory_id, report){
  //ドキュメントを格納されているフォルダオブジェクトを取得
  var targetFolder = getFolder(directory_id, Moment.moment());

  //ドキュメントオブジェクトを取得
  var doc_id = getFileId(targetFolder, Moment.moment().format("YYYY年M月D日") + department + name);

  if(doc_id){
    DocumentApp.openById(doc_id).getBody().appendParagraph(Moment.moment().format("HH:mm:ss") + "\t" + report + "\n");
  }
}

書き込みが終わったら、トリガーの時間を設定します。

trigger.gsより
function setTrigger(uuid, addMinutes){
  //このスクリプトのスクリプトIDを取得し、このスクリプトがある親フォルダのオブジェクトを取得します。
  var script_id = ScriptApp.getScriptId();
  var parentFolderIterator = DriveApp.getFileById(script_id).getParents();
  var parentFolderId = parentFolderIterator.next().getId();
  var parentFolder = DriveApp.getFolderById(parentFolderId);
  //親フォルダに入っているファイルイテレータリストを取得、指定の名前のスプレッドシートがあるかどうか検索します。
  var iteratorList = parentFolder.getFiles();

  var ss, spreadsheet;
  var fileExists = false;

  while (iteratorList.hasNext()){
    ss = iteratorList.next();
    if (ss.getName() == "Push管理用"){
      fileExists = true;
      break;
    }
  }
  //スプレッドシートがなければ作成し、スプレッドシートオブジェクトをspreadsheet変数に入れます。
  if (!fileExists){
    spreadsheet = SpreadsheetApp.create("Push管理用");
  }else{
    spreadsheet = SpreadsheetApp.openById(ss.getId());
  }
  //スプレッドシートに次に催促する時間を設定します。
  var sheets = spreadsheet.getSheets();

  for(var i in sheets){
    var sheet = sheets[i];
    if(sheet.getSheetId() == 0){
      var row = findRow(sheet, uuid, 1);
      if (row == 0){
        sheet.appendRow([uuid, Moment.moment().add(parseInt(addMinutes), "m").format("YYYY/MM/DD HH:mm:ss"), true]);
      }else{
        sheet.getRange(row, 2).setValue(Moment.moment().add(parseInt(addMinutes), "m").format("YYYY/MM/DD HH:mm:ss"));
        sheet.getRange(row, 3).setValue(true);
      }
    }
  }
  if (!fileExists){
    parentFolder.addFile(DriveApp.getFileById(spreadsheet.getId()));
    DriveApp.removeFile(DriveApp.getFileById(spreadsheet.getId()));
  }
}

2と5 休憩終了と業務終了のdelTrigger

setTriggerとほとんど同じです。今見たら参照する列の値をfalseに変えるだけですね。引数を変えるだけでよかったですね。。

common.gsより
function delTrigger(uuid, addMinutes){
  var script_id = ScriptApp.getScriptId();
  var parentFolderIterator = DriveApp.getFileById(script_id).getParents();
  var parentFolderId = parentFolderIterator.next().getId();
  var parentFolder = DriveApp.getFolderById(parentFolderId);

  var iteratorList = parentFolder.getFiles();

  var ss, spreadsheet;
  var fileExists = false;

  while (iteratorList.hasNext()){
    ss = iteratorList.next();
    if (ss.getName() == "Push管理用"){
      fileExists = true;
      break;
    }
  }
  if (!fileExists){
    spreadsheet = SpreadsheetApp.create("Push管理用");
  }else{
    spreadsheet = SpreadsheetApp.openById(ss.getId());
  }

  var sheets = spreadsheet.getSheets();

  for(var i in sheets){
    var sheet = sheets[i];
    if(sheet.getSheetId() == 0){
      var row = findRow(sheet, uuid, 1);
      if (row == 0){
        sheet.appendRow([uuid, Moment.moment().add(parseInt(addMinutes), "m").format("YYYY/MM/DD HH:mm:ss"), false]);
      }else{
        sheet.getRange(row, 2).setValue(Moment.moment().add(parseInt(addMinutes), "m").format("YYYY/MM/DD HH:mm:ss"));
        sheet.getRange(row, 3).setValue(false);
      }
    }
  }
  if (!fileExists){
    parentFolder.addFile(DriveApp.getFileById(spreadsheet.getId()));
    DriveApp.removeFile(DriveApp.getFileById(spreadsheet.getId()));
  }
}

5 報告書を上司に提出する。

    case 5:
      addLine(name, department, directory_id, "業務終了");
      setProp("access_token", decodeURI(JSON.parse(e.parameter.parameter2).access_token));
      sendReport(name, department, directory_id, decodeURI(JSON.parse(e.parameter.parameter2).mng_uid), getProp("access_token"));
      delTrigger(decodeURI(JSON.parse(e.parameter.parameter2).mng_uid), getProp("add_minutes"));
      break;

sendReportだけが1〜4と違うので書くと

control_document.gsより
function sendReport(name, department, directory_id, uid, channel_access_token){
  var title = Moment.moment().format("YYYY年M月D日") + department + name;
  //ドキュメントを格納されているフォルダオブジェクトを取得
  var targetFolder = getFolder(directory_id, Moment.moment());

  //ドキュメントオブジェクトを取得
  var doc_id = getFileId(targetFolder, title);

  if(doc_id){
    var doc = DocumentApp.openById(doc_id);
    //ファイルのURLを取得(Androidだとエラーで落ちる機種があるので不要な部分をトリミング)
    var fileUrl = doc.getUrl().replace(/\?usp=drivesdk$/, "");

    var message = department + " / " + name + "さんの日報が届きました。\nご確認ください。"
    //Pushメソッドを呼ぶ
    push(uid.split(","), message, channel_access_token, fileUrl);    
  }
}
common.gsより
function push(uid, message, channel_access_token, fileUrl) {
  var line_endpoint = "https://api.line.me/v2/bot/message/multicast";
  //引数にURLが渡されていたらFlex Messageで送ります。違ったらただのテキストで。
  if (fileUrl == ""){
    var postData = {
      "to" : uid,
      "messages" : [
        {
          "type" : "text",
          "text" : message,
        }
      ]
    };
  }else{
    var postData = {
      "to" : uid,
      "messages" : [
        {
          "type": "flex",
          "altText": message + "\n" + fileUrl,
          "contents": {
            "type": "bubble",
            "direction": "ltr",
            "body": {
              "type": "box",
              "layout": "vertical",
              "contents": [
                {
                  "type": "text",
                  "text": message,
                  "align": "start",
                  "gravity": "top",
                  "wrap": true
                },
                {
                  "type": "button",
                  "action": {
                    "type": "uri",
                    "label": "業務日報を見てみる",
                    "uri": fileUrl
                  },
                  "margin": "lg",
                  "style": "primary"
                }
              ]
            }
          }
        }
      ]
    };
  }
  var options_push = {
    "method" : "post",
    "headers" : {
      "Content-Type" : "application/json",
      "Authorization" : "Bearer " + channel_access_token,
    },
    "payload" : JSON.stringify(postData)
  };
  UrlFetchApp.fetch(line_endpoint, options_push);
}

その後の操作の説明です。

IFROに貼り付けるURLを取得する。

スクリーンショット 2020-03-30 17.47.07.png
「公開」 -> 「ウェブアプリケーションとして導入」
スクリーンショット 2020-03-30 17.47.18.png
「更新」を押します。
スクリーンショット 2020-03-30 17.49.30.png
自分のGoogleアカウントでログインします。
スクリーンショット 2020-03-30 17.49.40.png
「詳細を表示」を押した後、下に表示される「コピー〜LINE日報」に移動を押します。
スクリーンショット 2020-03-30 17.49.53.png
下にある「許可」を押すとURLが発行されます。
スクリーンショット 2020-03-30 17.47.30.png
この https ://script.google.com/macros/s/ から始まるURLをコピーしておきます。
このURLはあとでIFROのWebhookモジュールに貼り付けます。

催促用のトリガーを設定する。

trigger.gsより
//このメソッドを1分置きに実行
function reminder(){
  //管理用スプレッドシートを開く
  var script_id = ScriptApp.getScriptId();
  var parentFolderIterator = DriveApp.getFileById(script_id).getParents();
  var parentFolderId = parentFolderIterator.next().getId();
  var parentFolder = DriveApp.getFolderById(parentFolderId);

  var iteratorList = parentFolder.getFiles();

  var ss;
  var fileExists = false;

  while (iteratorList.hasNext()){
    ss = iteratorList.next();
    if (ss.getName() == "Push管理用"){
      fileExists = true;
      break;
    }
  }

  if (!fileExists){
    return;
  }

  var sheets = SpreadsheetApp.openById(ss.getId()).getSheets();
  //スプレッドシートの値を見ていき、催促する時間になっていたらPush送信を行う。
  for(var i in sheets){
    var sheet = sheets[i];
    if(sheet.getSheetId() == 0){
      var data = sheet.getDataRange().getValues(); 
      var sendList = "";

      for(var j in data){
        if(data[j][2] && Moment.moment(data[j][1]).isBefore(Moment.moment())){
          if(j == 0){
            sendList = data[j][0];
          }else{
            sendList += "," + data[j][0];
          }
          setTrigger(data[j][0], getProp("add_minutes"));
        }
      }
      //console.log("sendList : " + sendList);
      if(sendList.length != 0){
        var message = "今、何してますか?そろそろ一度報告してね!"
        var access_token = getProp("access_token");
        if (access_token != 0){
          push(sendList.split(","), message, access_token, "");
        }
      }
    }
  }
}

スクリーンショット 2020-03-30 17.59.18.png
この「時計」マークを押します。
すると別ウィンドウが立ち上がり、トリガーの設定画面になります。
スクリーンショット 2020-03-30 18.00.06.png
右下の「+トリガーを追加」ボタンを押します。
スクリーンショット 2020-03-30 18.01.07.png

  • 実行する関数:reminder
  • 実行するデプロイを選択:Head
  • イベントのソースを選択:時間主導型
  • 時間ベースのトリガーのタイプを選択:分ベースのタイマー
  • 時間の間隔を選択(分):1分おき

保存ボタンを押します。

IFRO②

最終的なプロパティ

スクリーンショット 2020-03-30 18.20.47.png

  • 上司のLINE uidは、以下で取得出来ます。

スクリーンショット 2020-03-30 18.21.03.png
LINE Developersにログインすると「チャネル基本設定」の最下部にあります。
上司にLINE IDでログインしてもらわなきゃいけませんね。。。
取得する方法を追加しておきます。

〜追加〜
「:LINE_ID:」とLINEボットに発話するとあなたのLINE IDを発話します。ので、上司に「:LINE_ID:」と言ってもらって返ってきたIDをプロパティに入れてください。

  • GoogleDriveのフォルダIDは、以下で取得出来ます。

スクリーンショット 2020-03-30 18.25.14.png

赤丸で囲んだ部分です。

URLを入れていく。

スクリーンショット 2020-03-30 18.14.28.png
残念ながらURLをプロパティにいれることが出来ない仕様な為、Webhookモジュールに先ほどコピーしたURLを入れていきます。
parameterは特にいれる必要はありません。
全部で5個のWebhookモジュールがあるので忘れずに!

以上で、終わりです!
スクリーンショット 2020-03-31 12.37.30.png
〜〜〜
スクリーンショット 2020-03-31 12.37.47.png

こんな感じです。

最後、ちょっと手抜きしましたが、わかりにくい部分があったら修正しますので言ってもらえたら。

追記

管理用のスプレッドシートを作成するコードが間違ってたので修正しました。

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

【0円ipadプログラミングの限界に挑む】iPad mini 5だけで本格的なweb開発出来るのかやってみたwwwww

qiita初投稿です。この記事ではipad一台だけでお金かけずにweb開発できるか検証したらどうなったのかを記したものであります。
スクショ及びgif画像多めです。
結果的にはこちら

チョット何言ってるの??

もうちょっと詳しく説明すると、HTMLやCSS、JavaScript更にはJqueryを使って、フロントエンドのweb開発をするということです。Githubも使います。

7.9インチのipadで


勿論、macやwindowsなどのパソコン使用禁止
なんでmini 5なのかというと、手元にあるapple端末がそれしか無いからです。
そんなこと出来るの!?と思った方もいれば、何とは言いませんが過去によく似た記事を見た人はそういえば!と思った方もいるかもしれません。
ですがその記事で紹介されたアプリは一部お金がかかってたのでこの記事で紹介するアプリは全て0円です。

検証

1 用意するもの

  • iPad
  • Bluetoothキーボード(これがないと辛い)

以上!!

2 githubにてRepositoryを作成する

Repositoryがある方は3にスキップ。
iPad版chromeかsafariでGitHubにログインします。
アカウントを持っていない方は「サインアップ」から作ってください。
7003BB5C-683C-4A13-BFCB-4949002B270B.jpeg
083A6CF3-03DD-433D-A281-F7CCB84859AC.png
Repositoryを作成します。
66A5D514-645B-42DF-BECD-07363C8D1700.jpeg
E3E84B11-1DC2-45CC-AEA4-03C0E3A67BDD.png
馴染みのある画面で安心しました。
Repository nameの所は必ず入れてください。僕は「ipadonly」という名前にしました。
create Repositoryを押すとRepositoryの作成が完了しました。
ただ、ここからかなり苦労したので出来るだけ詳しく説明していきます。

3 working copyをインストール~Repositoryの入手(?)

appstoreからworking copyというアプリを入手します。このアプリがあるからこその検証なのです!
8572CFEB-03ED-44E9-911D-35DAB371912B.png
開くとこんな感じ
9228D80E-5883-4ED1-8707-8B77AD903FD8.png
画面の青い文字のCreate(もしくは「Repositories」の右の+)を押すと吹き出しが出てきます。
「Clone repository」を選択します。するとウィンドウが出てくるので
1440AFE8-984E-4279-80D2-6207AAC940D7.jpeg
Sign Inをタップ。すると以下のようなウィンドウが開くので、
FAC5F482-2D21-4BB0-BBDB-40D51D662EC3.jpeg
メアドとパスワードを入力してサインイン!
すると~?
E005E2CC-B001-4259-9868-306E960A5324.jpeg
さっき作ったRepositoryが…出てきた!!!
もうこの時点で感動してきたのは僕だけじゃないはず...
1. Repositoryをタップ
2. URLがニョキっと上に移動するのかわいいすき
3. ウィンドウ右上の「Clone」が「Clone」になるのでタップ
するとッ!!?
82C2C487-FA97-435C-85BA-7D69BF0CBFE4.png
どうやらRepositoryを開けたようです!!!
ステップ3終わり!

4 いよいよweb開発

それでは早速、コードを書きたいと思います!!その前にフォルダを作成しましょう!
1. 「+」をタップ
2. Create directoryをタップ(何でdirectoryなんすかね)
3. 任意の名前を入力。僕は「tekito」にしました。
Enterを押すとフォルダの作成が完了し、勝手にフォルダが開きます。
ezgif-7-96cf3c9fb800.gif
ここから皆さんを驚かせます!

  1. 作成したフォルダを開き、+をタップ。
  2. 「Create text file」をタップします。
  3. 名前を「index」にしてみてください。 するとファイル名の隣に緑色の紙のアイコンが出てきました。画面右上が「plain text」になっているのが分かります。普通のテキストファイルですね。 4B778DEE-0E04-4ECD-911C-9F6EAD1FC95F.png それではもう一度、1と2の手順をしてファイルを作成し、 今度は名前を「index.html」にしてみてください。するとどうでしょう? khk.gif

VScodeやん!!!!!!???
なんと!?ファイル名の横のアイコンが紙ではなく「</>」になってる!
しかも画面右上の「plain text」が「HTML」に変化したの、分かりますか!?
これは間違いなくッ!!今僕が作成したファイルはHTML形式!!!
今度こそプログラムが書けるので、

index.html
<html>
 <head>
  <title></title>
 </head>
 <body>
 </body>
</html>

とりあえずこれだけ書いてみます。するとどうでしょう?
78274A79-3050-456D-A9F0-8909C260B668.png
シンタックスハイライトが付いてるだと!!!?
待ってくださいこれ書いたっていうかコピペして張り付けただけなんすけど!?
これってもしかして...
oh.gif
あっ( ^ω^)・・・
いえ、ただVScodeでお馴染みインテリセンスのような自動補完システムが搭載されているのではと思っただけなんです...なかった...

やっぱりインテリセンスがないとVScode使ってる身としては致命的ですねー...どうすれば

そうだ!appstoreで探すんだ!!!VScodeに負けない無料エディタアプリを!!!
「editor code」で検索っと…
ん?なんだこのアプリは?評価高いやんけ・・・(インスコする音)

!!!!!?!?!???!?

57AB9161-B4E2-457F-BB4D-2657458B2C07.png

キタああああああああああああああああんあんあんあんア”ンッ!!!!!!!


これを…これを求めてたんや!!!!!
今から全力で解説します!!!
まず、以下のアプリをインスコしてください。
FEFB1533-34C1-4AE4-9258-61CE1489410E.png
開きます。
FAD3F9C7-89C6-47CA-9A86-3E50EE8824D2.png
Open Koderをタップ。

ちなみにその上のFollow us nowをタップすると公式のツイッターに飛びます。ツイートの頻度少な過ぎたのでフォローはしませんでした。

52A1D263-B243-47E1-80C1-746467EF6897.png
画面左下の「+」をタップ。
98546165-414B-4551-8C4B-74C9A4DE76A4.jpeg
アイコン好き…
Open Other App’s fileをタップ
ブラウズ➡︎Working copy➡︎ipadonly➡︎tekito➡︎アイコンが「HTML」と書いてある紙の方のindex
の順番にタップします。すると先程開いたindex.htmlが開くかと思います。
66531398-4A92-4B49-804D-55FAEEFEAED3.gif
そしてカーソルを <diの後に持っていくだけでインテリセンスが表示されます!
いや〜これでようやく本格的なプログラミングが出来るわけですよ!
手始めに、色んなタグを使ってHello Worldの出力でもしてみましょう!!

index.html
<html>
 <head>
  <title></title>
 </head>
 <body>
 <div>
    <h1>Hello World!</h1>
    <h2>Hello World!</h2>
    <p>Hello World!</p>
</div>
 </body>
</html>

なんと自動的に保存されるのでctrl+sを押さなくていい!!
よし。じゃあクローム開いてindex.htmlをドラッグアンドドロッ
そういやプレビューってどうやるんや。。。
プレビューがないと話になりません。そんな時はworking copyを開いて下のgifの通りにします。
ezgif-7-3cdc30b3b7e1.gif
はい!これでプレビューが表示されました!!
え?何?クロームで表示したい!?
えっと!頑張って表示できるようにはしましたがお勧めできません。割と手間がかかりますし、バグが起きるリスクがあります(起きた)
なによりipadじゃないと無理です。
1. Koder code editorを再び開きます。
2. index.htmlは開いた状態にして、画面上部の歯車アイコンをタップ。
3. Local File Accessをタップ
4. Enable Local File AccessがONになっているか確認します。
5. 太字で「http://...」と書いてあるのが確認できます。その太字で書いてあるリンクをクロームの検索ボックスに貼り付け検索します。
この時、クロームとKoderを分割画面表示してください。
hah.gif
gif、見難いですがよく見るとindex.htmlの7行目が<h1>Hello!あsdhfjkl</h1>になっています。なのにクロームの方のプレビューをよく見るといまだにHello World!になっているのでやめたほうがいいです。
working copyに戻って、今度はindex.cssとindex.jsを作成します。
97B7ADAE-7656-4385-B15D-890EFDDF83AB.png
分割画面に対応してて偉い!
ちゃんとcssとjsのファイルになっていますねー
今度はhtmlとcssがちゃんと動くのか、index.htmlとindex.cssをいじってヘッダーを作りたいと思います。それぞれのファイルはこちらからどうぞ。(開きますかね?)
パソコンだとこんな感じ
an.png
簡単すぎたかな?
じゃあ次!ipadだとどうなるのか!?
7028AAB3-2DC4-4094-A4BA-374942192656.png
明朝体になってて草
それ以外は特に何も変わってなさそうですね!

jqueryは動くのか!?

jqueryはJavaScriptの書き方などを簡単にすることが出来ることで有名ですし、僕もjavascriptそっちのけで勉強してますw
ですが、ipadで書けるのでしょうか...?とりあえず、CDNでやってみたいと思います。
まず、index.html、.css、.jsをこちらの通りとします。
対照実験。パソコンだとjqueryでこんな感じで動いてます。
rd.gif
ではipadではどうんなふうに動くのか!?そもそも動くのか!!?
ene.gif
めっちゃ理想通りに動いてるやんけ…(´;ω;`)
いや~ここまできたらもう感無量です!無事にjquery動いてよかったです!ステップ4大成功!!

5 githubで色んな動作する

commitしてみる

まずcommitをしてみます。windowsではVScodeのターミナルを開いて色々コードを書いてローカルリポジトリにcommitしてpushをしました。
ターミナルの存在しないipadはどうなの?何かあったらやばいのでipadonlyとは別にsampleというリポジトリを作成。適当にhtmlファイルを作成。
画面上部を見る、と
283C9DF7-A647-4D64-8601-603982F2DC63.jpeg
色んなアイコンがありますね...
それらの中で左から3番目の指紋みたいなマークをタップ。
D147F061-0348-4379-8A82-C9C710CF68CE.jpeg
commitをタップ。
7408A66B-C6F6-4220-B21A-183E1660F03F.jpeg
上のテキストボックスにsummaryを、その次にdescriptionを入れる感じかな?
htmlファイルの隣のチェックボックスにチェックを入れてCommitを押せばローカルリポジトリへのcommitが完了します。

pushしてみる

ではリポジトリをプッシュしまsy...
題.png

うっそだろおおおおおおおおお:sob::sob::sob::sob:

どうやらリポジトリやファイルをgithubにpushする動作は有料だそうです。。。
ていうか、commit,revert,pull以外(多分)のgithubの全ての動作が有料でした。パソコンだと何気なく無料で出来るのにipadだと2440円するのはさすがにないと思いました。
ここまでか...いやまだ諦めてない...!!これはあくまでもworking copyなんや...
他のアプリで試すんや...
appstoreで探すんや...gitが使えるツールを...
これはどうかな?IMG_0327[1].PNG
アプリ開いて、サインインして、、、
真面目に説明します。(意味深)
1. サインインしたらヘッダーにNEWSとあります(なんだこれ)。左上の三本線のマークをタップ。
2. RepositoriesのOWNEDをタップ。
3. ipadonlyをタップ。
4. 一番下のSourceをタップ。
5. 編集したいファイルをタップ。(僕はsono2.mdのindex.htmlにしました。)
6. ファイルが開く。
7. 画面右上の共有マークをタップ。からのEditをタップ。
8. ファイルが編集できるようになるので適当に書く。(僕は37行目の「スマホアプリ」を「何か」に変えました。)
9. 編集し終わったら画面右上のsaveをタップ。
10. Upload...と書いてありますが、ここではプッシュのタイトルを記述します。記述し終わったらcommitをタップして完了。
lol.gif
さて、ホントにpushされているのか、windows版githubで確認しようかと思います。
へ.png
されてたああああああああああああああああ
やっときた...ここまできたら

pullもいけるやろ!

今度はpullが出来るかどうか試したいと思います。PCのVScodeでindex.htmlの22行目の「login」を「brain」にしてコードでプッシュ。そしてipadで
ezgi.gif
このアプリで開いたファイルをKoderで編集できるようにします。まず、上の5番の手順まで進めてindex.htmlが開いた状態にしておきます。
1. 画面右上の共有マークをタップ。からの「Open In」をタップ。
2. 横にスクロールして「その他」をタップ
3. Suggestionsの「Koderにコピー」をタップ
4. Koderが開く。Documentsの右下の+マークをタップ
5. New Folderをタップするとファイルを作成できるので、任意のファイル名を入力
6. Createをタップ。作成したファイルを開き、「Done」をタップすると、
初回はそのファイルにindex.htmlをコピーしたやつがインポートされる。
2回目以降は恐らく「ファイルが既に存在しますので上書きしますか?」みたいなことが聞かれるのでyesをタップして上書き。
99.gif

pushしてみる(2回目)

Koderで色々編集した後、command+Aで全選択してコピーしてCode hubでedit modeにしてまた全選択してペースト。するしか方法がないみたいです。効率悪いですね...。
22.gif
この記事もめっちゃ長くなったのでここで検証を終えたいと思います。

検証結果及び感想

今回紹介したアプリは3つ。

  • Working copy:以下、WKと略す。git連携ツール。ここでhtmlファイルなどを作成したり、プレビューを確認できる。2440円課金すればgitの全ての動作が可能。
  • Koder Code Editor:以下、Koder。ここでプログラムを書くことをお勧めする。
  • Code Hub:以下、CH。ここでgitの動作を行うことを勧める。
項目(?) 使ったアプリ 可能か
htmlファイルなどの作成 WK 可能
シンタックスハイライト WK、Koder、CH 可能
インテリセンス Koder、WK、CH Koderは可能 WKは✖
HtmlCssJsのプレビュー WK 可能
jquery WK,Koder 可能だがシンタックスハイライトが微妙
github commit WK,CH WK:ローカルリポジトリへのcommitは可能 CH:可能
push WK,CH WK:課金しないと無理 CH:なんか違う気がするが可能
pull WK,CH WK:課金しないと無理 CH:ゴリ押しでいける

正直ipadのみで、無課金でここまで出来るとは思っておりませんでした。最後にこの記事を書いた本当の目的なんですが、すべてここに書きました。文下手くそかもしれませんがお読みくだされば幸いです。

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

コードの写経したけど、動かない件

コードの写経したけど、動かねぇ...

そんなことは、初心者あるあるかと思います。

いちいち、自力で違いをチェックするのも面倒でしょう。

そんな時は、diffコマンドがおすすめ。(完成品ファイルがある時に限る。)

diffコマンドの使い方

diff [オプション] オリジナルファイル 新しいファイル

が基本です。

結果としては、例えば

1c1
< a
---
> b

などと返ります。

これは、オリジナルファイルの1行目がaから、新しいファイルの1行目がbに変わったという意味ですね。

ちなみに、変更が複数行にわたる時は、数字の部分が1,2とかなります。

コマンドの返りの意味

cは変更の意味ですが、dは削除、aは追加という意味ですね。

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

Vuexを初めから丁寧に(2)~Vuexの構成要素と使い方~

はじめに

前の記事ではVuexを理解するための前提知識(状態管理やデータフローについて)を見てきました。
本記事ではいよいよ、Vuex本体について入っていきます。

この記事を読むと

  • Vuexの構成要素やその使い方が分かる
  • Vuexを用いた本格的なアプリケーション開発に向けた準備が整う

想定読者

  • Vue.js や Nuxt.js の初級〜中級者
  • Vuex を何となく雰囲気で使っている

前提知識

Vuex による状態管理

いよいよ Vuex に入って行きます。

Vuex は Vue アプリケーション向けの状態管理ライブラリです。

Vuexは単なるライブラリとして機能を提供するだけではなく、公式に Vuex を使う際の実装のルールも示しており、それを含めてVuexです。
よって、例えば状態の更新はミューテーションでのみ行われるため、更新処理を探したいときはミューテーションを探せば良いです。
これには複数人で開発する際も既存のルールに従うだけで良いため設計やコミュニケーションの手間も省けます。

ストア

ストアは主にアプリの状態を保持する役割を担います。
その他にも状態管理に関する機能を盛り込んでおり、vuex の根幹となります。

// ストアの作成と代入
cosnt store = new Vuex.Store({オプション})

Vuexは信頼できる唯一の情報源であることを前提に設計されています。
アプリケーション内で常にただ一つのストアのみが存在するようにします。※
※一つのディレクトリという意味であり、必ずしもファイスが一つとは限りません。例えばNuxt.jsのモジュールモードにおいてはストア内に複数のJSファイルが存在します。詳しくはこの記事の後半、および次の記事で扱います。

ストアの構成要素

ストアの構成要素として、以下の4つの概念が存在します。

  • アプリのステート(State)
  • ステートの一部や、ステートから計算された値を返すゲッター(Getter)
  • ステートを更新するミューテーション(Mutation)
  • 主にAjaxリクエストのような非同期処理や、LocalStorageへの読み書きのような外部APIとのやり取りを行うアクション(Action)

ステートは状態、ミューテーションは更新処理に対応します。

規模の大きいアプリを作る際には、上記4つの構成要素をモジュール(Module)という単位で分割して見通しを良くします。
アプリの状態を全て一つの場所に置いてしまうと逆に管理が大変になるのではないかと感じるかもしれませんが、モジュール※を使うことで、信頼できる唯一の情報源を守りながら状態やそれに関わる更新、取得のロジックを複数の単位に分割をし、管理をシンプルに行えます。

ステート

ステートの概要

Vuexのステートはアプリ全体の状態を保持するオブジェクトです。
全てのステートは一つの木構造として表現されます。
アプリケーションの全ての状態を一つの木としてステートに保持することで、「信頼できる唯一の情報源」として機能します。
しかし、アプリケーションの全ての状態を必ずしもVuexで管理するべきではありません。Vuexのステートで管理するべきデータと、そうでないデータの例は下記の通りです。

ステートに適したデータ
- ログイン中のユーザーの情報など、アプリ全体で使用されるデータ
- ecサイトにおける商品の情報など、アプリの複数の場所で使用される可能性のあるデータ

コンポーネント側で持つべきデータ
- マウスポインタがある要素の上に存在するかどうかを、表すフラグ
- ドラッグ中の要素の座標
- 入力中のフォームの値

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

// ストアの定義
const store = new Vuex.Store({
  state: {
count: 10
}

ステートはコンポーネントのdataオプションに渡された値と同じように変更が追跡されます。
ステートに対して何らかの更新を行うとその変更は自動的にコンポーネントの算出プロパティやテンプレートへと反映されます。
これはVuexが内部的にVueのリアクティブシステムを活用して実装されているためです。
また、ステート内の依存関係がリアクティブシステムによって計算されるため、ステート更新時のuiの再描画が必要最小限になるというメリットもあります。

ゲッター

ゲッターの概要

ゲッターはステートから別の値を算出するために用いられます。例えばユーザーの操作によって商品のリストを絞り込みたい時にはゲッターで絞り込んだ商品のリストを算出します。
ゲッターを使用することでコンポーネント上で表示のためにステートを計算することが避けられ、異なるコンポーネント間でロジックを再利用できるようになります。

ゲッターの定義方法

gettersオプションに関数をもつオブジェクトを指定することでゲッターを定義します。
コンポーネントの算出プロパティとよく似た機能ですが、引数にステートと他のゲッターが渡され、それらを使った値を返す点が異なります。

ゲッターの使い方

ゲッターはstore.gettersから参照できます。

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

// ストアの定義
const store = new Vuex.Store({
  state: {
count: 10
},
// gettersオプションでgettersを定義する
getters: {
// ステートから別の値を算出する
squared: (state) => state.count* getters.squared
}
})

ゲッターはコンポーネントのcomputedオプションと同様に評価された値がキャッシュされます。
キャッシュされた値はそのゲッターが依存しているステートが更新されない限り再評価されません。

したがって、よく使用するステートの算出ロジックはゲッターにすることでパフォーマンスの向上が期待できます。一方でゲッターを参照したときに定義した関数が常に実行されるわけではありません。
例えば依存するステートが存在しない時にサーバーから値を取得するというような処理はゲッターの中には書かず、続けて解説するミューテーション、アクションを使って取得とステートへの反映を行います。

ミューテーション

ミューテーションの概要

ミューテーションはステートを更新するために用いられます。
Vuexでは規約としてミューテーション以外がステートの更新を行うことを禁止しています。
ステートの更新をミューテーションのみが行えば、ステートの変更がいつどこで発生したのかを追跡しやすくなります。

ミューテーションの定義方法

mutationsオプションにミューテーション名をキーに持ち、ハンドラー関数を値に持つオブジェクトを指定することでミューテーションを定義できます。
ハンドラー内では第一引数に渡されたステートを更新します。

ミューテーションの使い方

ミューテーションは直接は呼び出せません。store.commitにミューテーション名を与えて呼び出します。
これはイベントの発生と監視によく似ています。
incrementというイベントが発生したときに、その名前で登録したミューテーションハンドラーが実行されると考えると分かりやすいです。

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

// ストアの定義
const store = new Vuex.Store({
  state: {
count: 10
},
// gettersオプションでgettersを定義する
getters: {
// ステートから別の値を算出する
squared: (state) => state.count* getters.squared
 },
 mutations: {
// 'incremation'ミューテーションを定義
increment(state) {
state.count = state.count + 1
 }
}
})

store.commitの第二引数になんらかの値を与えるとそれがハンドラーの第二引数に渡されます。この値のことをペイロード(payload)※と呼びます。ペイロードを使用することで、同じミューテーションでも渡す値によって異なるステートに更新できます。

※payloadの意味は?

ミューテーション内で行う処理は非同期を用いると意図しない動作を引き起こす可能性があるため、全て同期的にする必要があります。
非同期処理を行う必要があるときは次に紹介するアクションを代わりに使用します。

アクション

アクションの概要

アクションは非同期処理や外部APIとの通信を行い、最終的にミューテーションを呼びだすために用いられます。

アクションの定義方法

actionsオプションにアクション名をキーに持ち、ハンドラー関数を値に持つオブジェクトを指定することでアクションを定義します。

アクションの使い方

アクションはミューテーションと同様に直接呼び出すことはできません。store.dispatchにアクション名を渡して呼び出します。

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

// ストアの定義
const store = new Vuex.Store({
  state: {
count: 10
},
// gettersオプションでgettersを定義する
getters: {
// ステートから別の値を算出する
squared: (state) => state.count* getters.squared
 },
 mutations: {
// 'incremation'ミューテーションを定義
increment(state) {
state.count = state.count + 1
 }
},
// acitionsオプションでアクションを定義する
actions: {
incrementAction(ctx) {
// incrementミューテーションを実行する
ctx.commit('increment')
}
})

アクションの定義はミューテーションとよく似ています。ただし、ハンドラーの第一引数にステートではなくコンテキスト(context)と呼ばれる特別なオブジェクトが渡される点で異なります。
コンテキストには以下が含まれます。

  • state: 現在のステート
  • getters: 定義されているゲッター
  • dispatch: 他のアクションを実行するメソッド
  • commit: ミューテーションを実行するメソッド

stateやgettersは、例えばデータのロード中にはアクションの処理を行わないというような現在の状態に応じてアクションの処理を切り替えるときに使います。
dispatchを使うことで、すでに定義してある他のアクションを呼びだせます。
これによって共通の処理を一つのアクションにまとめることができますが、使い過ぎるとどのアクションから呼ばれているのか分かりづらくなるので気をつけましょう。
アクションはミューテーションを実行するのに用いられるため、commitが使われることが最も多いでしょう。

以下はAjaxでデータを取得し、そのデータをペイロードに含めたミューテーションを呼び出すアクションを定義している例です。第一引数のコンテキストを分割代入({ commit })することで短い記法で書かれています。また、ミューテーションと同様にアクションも第二引数にペイロードを受け取ります。

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

// ストアの定義
const store = new Vuex.Store({
  state: {
count: 10
},
// gettersオプションでgettersを定義する
getters: {
// ステートから別の値を算出する
squared: (state) => state.count* getters.squared
 },
 mutations: {
// 'incremation'ミューテーションを定義
increment(state) {
state.count = state.count + 1
 }
},
// acitionsオプションでアクションを定義する
actions: {
incrementAction(ctx) {
// incrementミューテーションを実行する
ctx.commit('increment')
}
})

おわりに

如何だったでしょうか。簡単に復習してみましょう。
- Vuexはステート、ゲッター、ミューテーション、アクションの4つの要素で構成される
- それぞれの要素の定義方法と使い方を覚える

この内容がバッチリでしたら、Vuexの基本的な使い方が頭に入ったことになります。
次の記事では、さらにより実践的な応用知識について見ていきます。例えばNuxt.jsにおけるVuexの扱いについてです。

参考文献

『Vue.js入門 基礎から実践アプリケーション開発まで』(川口和也, 喜多啓介, 野田陽平, 手島拓也, 片山真也/技術評論社)
Vue.jsについての書籍は増えてきていますが、問題なのはその殆どがVuexについての説明を省略していることです。Vue.jsやNuxt.jsを用いた実際の開発においてVuexによる状態管理は必須ですが、学習の障壁になるとして避けてしまっているのでしょう。私が読んだ中で唯一、Vuexについて丁寧に説明していたのが本書です。Vuex以外の内容も素晴らしいの一言。本書はVue.js・Nuxt.jsの開発に関わるエンジニアや組織にとって必携です。保存用・実用用・観賞用に3冊購入しましょう。あるいは、あなたが経営者の場合はぜひエンジニアに対して一人一冊ずつ買い与えてください。
ただし、全くVueについて未経験という方への第一歩としては内容が本格的すぎるかもしれません。その場合は『Vue.js 超入門』がおすすめです。

『Vue.js 超入門』(掌田津耶乃/秀和システム)
とにかく分かりやすく、まず概要を把握するために最適の一冊です。「なんとなくで良いので概要を把握する」⇨「より詳細で厳密な理解する」という流れで学ぶとスムーズです。

『初めてのJavaScript 第3版 ―ES2015以降の最新ウェブ開発』(Ethan Brown, 武舎広幸,武舎るみ/オライリージャパン)
JavaScriptの根本的な理解ができる、革命的な良書です。分厚いので手強そうに見えますが、実際はとても親切で分かりやすい作りです。本書も一人一冊は欲しいところです。

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

【JavaScript】ベジェ曲線を使ったアニメーション【Canvas】

はじめに

canvasでアニメーションすることって多いと思います。
今回はある点を直線的に移動するときのアニメーションを、
ベジェ曲線を使って定義できるようにしたいと思います。
本記事は少しはベジェ曲線について知っている人向けの記事です。

ベジェ曲線についての簡単な説明

簡単にベジェ曲線について説明します。
今回扱うのは3次ベジェ曲線です。
横軸をx,縦軸をyとします。
3次ベジェ曲線には、制御点が4つあります。
制御点の座標が(0, 0), (0, 0.5), (0.5, 1), (1, 1)のベジェ曲線は以下のようになります
(青色の曲線がベジェ曲線、青色の点と緑色の点が制御点です)

----.png

3次ベジェ曲線は、パラメータtを使って表されます。
tは0以上、1以下の値をとります。

t=0.3のときのベジェ曲線上の点を求めてみます。

点を4つから3つに減らします。
(0, 0)と(0, 0.5)を0.3:0.7に分割する点を求めます => (0, 0.15)
(0, 0.5)と(0.5, 1)を0.3:0.7に分割する点を求めます => (0.15, 0.65)
(0.5, 1)と(1, 1)を0.3:0.7に分割する点を求めます => (0.65, 1)

点を3つから2つに減らします。
(0, 0.15)と(0.15, 0.65)を0.3:0.7に分割する点を求めます => (0.045, 0.3)
(0.15, 0.65)と(0.65, 1)を0.3:0.7に分割する点を求めます => (0.3, 0.755)

点を2つから1つに減らします。
(0.045, 0.3)と(0.3, 0.755)を0.3:0.7に分割する点を求めます => (0.1215, 0.4365)0.3.png
この点がt=0.3のベジェ曲線上の点となります。

t = 0.5の場合
0.5.png

t = 0.8の場合
0.8.png

t = 0 からt = 1 まで0.1刻みでベジェ曲線上の点を表示
all.png

アニメーションをtとベジェ曲線のy値で定義する

jQueryのanimate関数やCSSのcubic-bezierのようなアニメーションを実現させましょう。
今、我々はtが定まれば、制御点からベジェ曲線上の点を求める方法を知っています。
円を点Aから点Bへアニメーションする場合、
パラメータtをアニメーション時間の割合とし、ベジェ曲線の点のY座標を点Aからの移動の割合とすればうまくいきそうです。
点Aの位置ベクトルをa,点Bの位置ベクトルをbとすれば、
a+(b-a) * (パラメータtのベジェ曲線のY座標) です。

tについて

点Aから点Bへ2秒かけてアニメーションする場合、
60fpsで現在のフレームがn(1≦n≦120,nは整数)なら
t = n / (2 * 60) となる

プログラム的にはどう書くか

// tが0.2のときのベジェ曲線のY値を求める

const points = [
    { x: 0, y: 0 },    // この点は固定
    { x: 0, y: 0.5 },  // この点は自由に設定してください
    { x: 0, y: 0.5 },  // この点は自由に設定してください
    { x: 1, y: 1 },    // この点は固定
];
const ret = getCubicBezierPoint(points, 0.3); // ret.yがベジェ曲線のY値です。これをベクトルABにかければよい。

再現可能なイージング

こちらにあるcubic-bezierの引数を、
上に書いたプログラムのpoints[1],points[2]に代入すれば再現できます。

全ソース

このプログラムは、制御点を動かし、それに伴うベジェ曲線のY値の変化を見るプログラムです。
その前にちょっと説明。
t軸をx軸に重ねて描画しています。
アニメーション時に灰色の点がX軸に沿って動き、
赤い点がベジェ曲線とY軸に沿って動くと思います。
灰色の点はtを表していることに注意してください。

app.png

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>cubic-bezier</title>
<script src="./lib/temp/jquery-3.1.1.min.js" type="text/javascript"></script>
<style>
body {
    overflow: hidden;
}
#input-area {
    position: absolute;
    left: 0;
    top: 0;
    z-index: 100;
}
#canvas {
    position: absolute;
    left: 0;
    top: 0;    
}
#demo-area {
    position: absolute;
    left: 0;
    top: 800px;
    z-index: 100;
}
</style>
<script>

// 行列クラス(行列の計算に使用)
class Matrix {
    // m0は行列、m1は行列又はベクトル
    // 行列は大きさ9の1次元配列であること。 ex. [ 1, 0, 0, 0, 1, 0, 0, 0, 1 ]
    // ベクトルはxとyをプロパティに持つ連想配列であること。 ex. { x: 2, y: 4 }
    // 左からベクトルをかけることは想定していない
    static multiply(m0, m1) {
        if(m1.length && m1.length === 9) {// m1は行列
            return [
                m0[0] * m1[0] + m0[1] * m1[3] + m0[2] * m1[6],
                m0[0] * m1[1] + m0[1] * m1[4] + m0[2] * m1[7],
                m0[0] * m1[2] + m0[1] * m1[5] + m0[2] * m1[8],
                m0[3] * m1[0] + m0[4] * m1[3] + m0[5] * m1[6],
                m0[3] * m1[1] + m0[4] * m1[4] + m0[5] * m1[7],
                m0[3] * m1[2] + m0[4] * m1[5] + m0[5] * m1[8],
                m0[6] * m1[0] + m0[7] * m1[3] + m0[8] * m1[6],
                m0[6] * m1[1] + m0[7] * m1[4] + m0[8] * m1[7],
                m0[6] * m1[2] + m0[7] * m1[5] + m0[8] * m1[8],
            ];
        } else {// m1はベクトル
            return {
                x: m0[0] * m1.x + m0[1] * m1.y + m0[2],
                y: m0[3] * m1.x + m0[4] * m1.y + m0[5],                
            };
        }
    }
    // 単位行列
    static identify() {
        return [1, 0, 0, 0, 1, 0, 0, 0, 1];
    }
    // 平行移動行列
    static translate(x, y) {
        return [1, 0, x, 0, 1, y, 0, 0, 1];
    }
    // 拡大縮小行列
    static scale(x, y) {
        return [x, 0, 0, 0, y, 0, 0, 0, 1];
    }
    // 回転行列
    static rotate(theta) {
        const cos = Math.cos(theta),
            sin = Math.sin(theta);
        return [cos, -sin, 0, sin, cos, 0, 0, 0, 1];
    }
    // 逆行列を求める
    static inverse(m) {
        const det = Matrix.determinant(m),
            inv = [
                m[4] * m[8] - m[5] * m[7],    -(m[1] * m[8] - m[2] * m[7]),   m[1] * m[5] - m[2] * m[4],
                -(m[3] * m[8] - m[5] * m[6]), m[0] * m[8] - m[2] * m[6],      -(m[0] * m[5] - m[2] * m[3]),
                m[3] * m[7] - m[4] * m[6],    -(m[0] * m[7] - m[1] * m[6]),   m[0] * m[4] - m[1] * m[3]
            ];
        return inv.map(elm => elm / det);
    }
    // 行列式を求める
    static determinant(m) {
        return m[0] * m[4] * m[8] 
        + m[1] * m[5] * m[6] 
        + m[2] * m[3] * m[7]
        - m[2] * m[4] * m[6]
        - m[1] * m[3] * m[8]
        - m[0] * m[5] * m[7];
    }
}

// ベクトルクラス(ベクトルの計算に使用)
class Vector {
    // 足し算
    static add(v0, v1) {
        return {
            x: v0.x + v1.x,
            y: v0.y + v1.y,
        };
    }
    // 引き算
    static subtract(v0, v1) {
        return {
            x: v0.x - v1.x,
            y: v0.y - v1.y,
        };
    }
    // 掛け算
    static scale(v, s) {
        return {
            x: v.x * s,
            y: v.y * s,
        };
    }
    // ベクトルの長さを返す
    static length(v) {
        return Math.sqrt(v.x * v.x + v.y * v.y);
    }
    // 単位ベクトルを返す(非破壊的)
    static unit(v) {
        const len = Vector.length(v);
        return {
            x: v.x / len,
            y: v.y / len
        };
    }
    // 内積
    static innerProduct(v0, v1) {
        return v0.x * v1.x + v0.y + v1.y;
    }
}

$(() => {    

    const points = [
        { x: 0, y: 0 },
        { x: 0, y: 0.5 },
        { x: 0.5, y: 1 },
        { x: 1, y: 1 }
    ];

    // 2つの制御点(0,0), (1,1)は固定
    // 他の2つの制御点は動作可能
    const fixedIndexes = [0, 3];

    let mode = '';
    let worldPrePos;
    let pickedIndex;
    let animCheck = true;
    let animCnt = 0;
    let demoAnimFrame = 0;

    const FIXED_POINT_COLOR = 'blue';
    const MOVE_POINT_COLOR = 'green';
    const TIME_POINT_COLOR = 'gray';
    const CURRENT_POINT_COLOR = 'red';
    const POINT_RADIUS = 5;
    const BEZIER_DIVIDE_COUNT = 100;
    const DEMO_INTERVAL = 2;    // デモは2秒で動く

    let m = Matrix.identify();
    const m1 = Matrix.scale(400, 400);
    const m2 = Matrix.scale(1, -1);
    const m3 = Matrix.translate(0.2, -1.2);

    m = Matrix.multiply(m, m1);
    m = Matrix.multiply(m, m2);
    m = Matrix.multiply(m, m3);

    $('#canvas').prop({
        width: window.innerWidth,
        height: window.innerHeight,
    });

    $('#anim-check').prop({ checked: animCheck });

    $('#anim-check').on('change', e => {
        animCheck = $('#anim-check').prop('checked');
        console.log(animCheck);
    });

    $('#canvas').on('mousedown', e => {
        if(mode) { return; }
        e.preventDefault();
        const cursorPos = { x: e.pageX, y: e.pageY };

        const inv = Matrix.inverse(m);
        worldPrePos = Matrix.multiply(inv, cursorPos);
        pickedIndex = pick(worldPrePos, 2 * POINT_RADIUS / 400);
        if(pickedIndex !== -1) {// ピック出来た
            mode = 'picked';
        } 
    });

    $(window).on('mousemove', e => {
        if(!mode) { return; }

        const cursorPos = { x: e.pageX, y: e.pageY };

        const inv = Matrix.inverse(m);
        const cursorWorldPos = Matrix.multiply(inv, cursorPos);

        const worldVec = Vector.subtract(cursorWorldPos, worldPrePos);

        points[pickedIndex] = Vector.add(points[pickedIndex], worldVec);

        worldPrePos = cursorWorldPos;
    });

    $(window).on('mouseup', e => {
        if(!mode) { return; }
        mode = '';
    });

    $('#demo-move-button').on('click', e => {
        demoAnimFrame = 1;
    });

    anim();

    function pick(worldPos, radius) {
        return points.findIndex((p, i) => {
            if(fixedIndexes.indexOf(i) >= 0) {// 固定
                return false;
            } else {
                const v = Vector.subtract(p, worldPos)
                const len = Vector.length(v);
                return len <= radius;
            }            
        });
    }

    function anim() {
        const ctx = $('#canvas')[0].getContext('2d'); 

        ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);

        // X軸, Y軸を描画
        drawLine(ctx, [{ x: 0, y: 0, }, { x: 1, y: 0, }].map(p => Matrix.multiply(m, p)), 1, 'black');
        drawLine(ctx, [{ x: 0, y: 0, }, { x: 0, y: 1, }].map(p => Matrix.multiply(m, p)), 1, 'black');

        // ベジェ曲線を描画
        drawCubicBezierLines(ctx, points.map(p => Matrix.multiply(m, p)), BEZIER_DIVIDE_COUNT, 2, 'blue');

        // 制御点結ぶ線分を描画
        points.forEach((p, i) => {
            if(i >= points.length - 1) { return; }
            // 点の色を決定
            drawLine(ctx, [p, points[i + 1]].map(p => Matrix.multiply(m, p)), 1, '#888');
        }); 

        // 制御点を描画
        points.forEach((p, i) => {
            // 点の色を決定
            let color;
            if(fixedIndexes.indexOf(i) >= 0) {// 固定
                color = FIXED_POINT_COLOR;
            } else {// 動作可能
                color = MOVE_POINT_COLOR;
            }

            drawPoint(ctx, Matrix.multiply(m, p), POINT_RADIUS, color);
        }); 

        // 点情報を描画
        points.forEach((p, i) => {            
            const base = { x: 20, y: 600 + i * 40 };
            drawPointInformation(ctx, p, base, i, '24px serif', 'black');
        });  

        if(animCheck) { 
            // 時間に沿って動く点を描画
            const t = animCnt / 100;
            const tp = getCubicBezierPoint(points, t);
            drawPoint(ctx, Matrix.multiply(m, tp), POINT_RADIUS, CURRENT_POINT_COLOR);

            // 点をY軸に射影
            const ytp = { x: 0, y: tp.y };
            drawPoint(ctx, Matrix.multiply(m, ytp), POINT_RADIUS, CURRENT_POINT_COLOR);

            // 時間(t)をX軸に表示
            const xtp = { x: t, y: 0 };
            drawPoint(ctx, Matrix.multiply(m, xtp), POINT_RADIUS, TIME_POINT_COLOR);  

            animCnt += 1;   
            if(animCnt > 100) {
                animCnt = 0;
            }     
        }

        // 下側の始点終点を描画
        drawPoint(ctx, { x: 100, y: 800 }, 10, 'gray');
        drawPoint(ctx, { x: 600, y: 800 }, 10, 'gray');

        // 下側の現在の点を描画
        const t = demoAnimFrame / (DEMO_INTERVAL * 60);
        const point = getCubicBezierPoint(points, t);
        const start = { x: 100, y: 800 };
        const end = { x: 600, y: 800 };
        const vec = Vector.subtract(end, start);
        const cur = Vector.add(start, Vector.scale(vec, point.y));
        drawPoint(ctx, cur, 10, 'red');

        // 下の点を動かす
        if(demoAnimFrame > 0) {
            demoAnimFrame += 1;
            if(demoAnimFrame % (DEMO_INTERVAL * 60) === 0) {
                demoAnimFrame = 0;
            }
        } 

        requestAnimationFrame(anim);  
    }

    // 点の座標情報を描画
    function drawPointInformation(ctx, point, base, index, font, color) {
        ctx.font = font;
        ctx.fillStyle = color;
        // 項目
        ctx.fillText(`p[${index}]`, base.x, base.y);
        // X座標
        ctx.fillText(`X: ${point.x.toFixed(4)}`, base.x + 100, base.y);
        // Y座標
        ctx.fillText(`Y: ${point.y.toFixed(4)}`, base.x + 100 + 200, base.y);
    }    

    // パラメータを指定して3次ベジェ曲線上の座標を求める
    function getCubicBezierPoint(points, t) {
        let sub0, sub1, sub2;

        if(t === 0) {
            return points[0];
        } else if(t === 1) {
            return points[3];
        }

        sub0 = subdivide([ points[0], points[1] ], t);
        sub1 = subdivide([ points[1], points[2] ], t);
        sub2 = subdivide([ points[2], points[3] ], t);

        sub0 = subdivide([ sub0, sub1 ], t);
        sub1 = subdivide([ sub1, sub2 ], t);

        return subdivide([ sub0, sub1 ], t);

        // 内分点を求める
        function subdivide(points, t) {
            return {
                x: (1 - t) * points[0].x + t * points[1].x,
                y: (1 - t) * points[0].y + t * points[1].y,
            };
        }
    }

    // 3次ベジェ曲線を微小線分に分割して描画
    function drawCubicBezierLines(ctx, points, divides, width, color) {

        ctx.save();

        ctx.strokeStyle = color;
        ctx.lineWidth = width;

        ctx.beginPath();

        for(let i = 0; i <= divides; i += 1) {
            const t = i / divides;

            // パラメータを指定して3次ベジェ曲線上の座標を求める
            const bezierPoint = getCubicBezierPoint(points, t);

            if(i === 0) {
                ctx.moveTo(bezierPoint.x, bezierPoint.y);
            } else {
                ctx.lineTo(bezierPoint.x, bezierPoint.y);
            }
        }

        ctx.stroke();

        ctx.restore();
    }

    // 点を描画
    function drawPoint(ctx, point, radius, color) {
        ctx.save();

        ctx.fillStyle = color;

        ctx.beginPath();
        ctx.arc(point.x, point.y, radius, 0, 2 * Math.PI);
        ctx.closePath();
        ctx.fill();

        ctx.restore();
    }

    // 線分を描画
    function drawLine(ctx, points, width, color) {
        if(points.length !== 2) { return; }

        ctx.save();

        ctx.strokeStyle = color;
        ctx.lineWidth = width;

        ctx.beginPath();
        ctx.moveTo(points[0].x, points[0].y);
        ctx.lineTo(points[1].x, points[1].y);
        ctx.stroke();

        ctx.restore();
    }
});

</script>
</head>
<body>
<div id="input-area">
    <input id="anim-check" type="checkbox" />
    <label for="anim-check">anim</label>
</div>
<div id="demo-area">
    <input id="demo-move-button" type="button" value="move" />
</div>
<!-- メインのcanvas -->
<canvas id="canvas"></canvas>
</body>
</html>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Axiosのrequest/responseの情報をログに出力

Axiosのinterceptorsを使ってrequest/responseの情報をログに出力する場合のコード例です。
ログの部分をクラウドやログファイルへの出力に書き換えることで、request/responseの記録を残すことが可能です。

client.ts
const client = axios.create({
    baseURL: "https://xxx.yyy",
});

client.interceptors.request.use((config: AxiosRequestConfig) => {
    const url = `${config.baseURL}${config.url}`;
    console.log(`Method=${config.method} Url=${url} Body=${JSON.stringify(config.data)}`);
    return config;
});

client.interceptors.response.use(
    (response: AxiosResponse) => {
        const status = response.status;
        console.log(`Success: Status=${status}`);
        return response;
    },
    (error: AxiosError) => {
        const status = error.response!.status;
        const message = error.response!.data.messages[0] || "No message.";
        console.log(`Error: Status=${status} Message=${message}`);
        throw new HttpRequestError(status, message);
    }
);

日時とRequestIdを加えて以下のように出力しています。

2020-03-24 05:31:49 +0000 [716de8c0-78a6-4387-881c-17a2ca903f55]: Method=get Url=https://xxx.yyy/zzz Body=undefined
2020-03-24 05:31:49 +0000 [716de8c0-78a6-4387-881c-17a2ca903f55]: Error: Status=404 Message=xxx does not exist.
2020-03-24 05:31:49 +0000 [716de8c0-78a6-4387-881c-17a2ca903f55]: Method=get Url=https://xxx.yyy/zzz Body=undefined
2020-03-24 05:31:49 +0000 [716de8c0-78a6-4387-881c-17a2ca903f55]: Success: Status 200

より詳細な情報を出力したい場合はAxiosの型定義ファイルを参照してください。
出力したい情報が見つかるかもしれませんね。

index.d.ts
export interface AxiosResponse<T = any>  {
  data: T;
  status: number;
  statusText: string;
  headers: any;
  config: AxiosRequestConfig;
  request?: any;
}

export interface AxiosError<T = any> extends Error {
  config: AxiosRequestConfig;
  code?: string;
  request?: any;
  response?: AxiosResponse<T>;
  isAxiosError: boolean;
  toJSON: () => object;
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Axiosmでrequest/responseの情報をログに出力

Axiosのinterceptorsを使ったrequest/responseの情報をログに出力する場合のコード例です。
ログの部分をクラウドやログファイルへの出力に書き換えることで、request/responseの記録を残すことが可能です。

client.ts
const client = axios.create({
    baseURL: "https://xxx.yyy",
});

client.interceptors.request.use((config: AxiosRequestConfig) => {
    const url = `${config.baseURL}${config.url}`;
    console.log(`Method=${config.method} Url=${url} Body=${JSON.stringify(config.data)}`);
    return config;
});

client.interceptors.response.use(
    (response: AxiosResponse) => {
        const status = response.status;
        console.log(`Success: Status=${status}`);
        return response;
    },
    (error: AxiosError) => {
        const status = error.response!.status;
        const message = error.response!.data.messages[0] || "No message.";
        console.log(`Error: Status=${status} Message=${message}`);
        throw new HttpRequestError(status, message);
    }
);

実際のログでは日時とRequestIdを加えて以下のように出力しています。

2020-03-24 05:31:49 +0000 [716de8c0-78a6-4387-881c-17a2ca903f55]: Method=get Url=https://xxx.yyy/zzz Body=undefined
2020-03-24 05:31:49 +0000 [716de8c0-78a6-4387-881c-17a2ca903f55]: Error: Status=404 Message=xxx does not exist.
2020-03-24 05:31:49 +0000 [716de8c0-78a6-4387-881c-17a2ca903f55]: Method=get Url=https://xxx.yyy/zzz Body=undefined
2020-03-24 05:31:49 +0000 [716de8c0-78a6-4387-881c-17a2ca903f55]: Success: Status 200

より詳細な情報を出力したい場合はAxiosの型定義ファイルを参照してください。
出力したい情報が見つかるかもしれませんね。

index.d.ts
export interface AxiosResponse<T = any>  {
  data: T;
  status: number;
  statusText: string;
  headers: any;
  config: AxiosRequestConfig;
  request?: any;
}

export interface AxiosError<T = any> extends Error {
  config: AxiosRequestConfig;
  code?: string;
  request?: any;
  response?: AxiosResponse<T>;
  isAxiosError: boolean;
  toJSON: () => object;
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Axiosでrequest/responseの情報をログに出力

Axiosのinterceptorsを使ったrequest/responseの情報をログに出力する場合のコード例です。
ログの部分をクラウドやログファイルへの出力に書き換えることで、request/responseの記録を残すことが可能です。

client.ts
const client = axios.create({
    baseURL: "https://xxx.yyy",
});

client.interceptors.request.use((config: AxiosRequestConfig) => {
    const url = `${config.baseURL}${config.url}`;
    console.log(`Method=${config.method} Url=${url} Body=${JSON.stringify(config.data)}`);
    return config;
});

client.interceptors.response.use(
    (response: AxiosResponse) => {
        const status = response.status;
        console.log(`Success: Status=${status}`);
        return response;
    },
    (error: AxiosError) => {
        const status = error.response!.status;
        const message = error.response!.data.messages[0] || "No message.";
        console.log(`Error: Status=${status} Message=${message}`);
        throw new HttpRequestError(status, message);
    }
);

実際のログでは日時とRequestIdを加えて以下のように出力しています。

2020-03-24 05:31:49 +0000 [716de8c0-78a6-4387-881c-17a2ca903f55]: Method=get Url=https://xxx.yyy/zzz Body=undefined
2020-03-24 05:31:49 +0000 [716de8c0-78a6-4387-881c-17a2ca903f55]: Error: Status=404 Message=xxx does not exist.
2020-03-24 05:31:49 +0000 [716de8c0-78a6-4387-881c-17a2ca903f55]: Method=get Url=https://xxx.yyy/zzz Body=undefined
2020-03-24 05:31:49 +0000 [716de8c0-78a6-4387-881c-17a2ca903f55]: Success: Status 200

より詳細な情報を出力したい場合はAxiosの型定義ファイルを参照してください。
出力したい情報が見つかるかもしれませんね。

index.d.ts
export interface AxiosResponse<T = any>  {
  data: T;
  status: number;
  statusText: string;
  headers: any;
  config: AxiosRequestConfig;
  request?: any;
}

export interface AxiosError<T = any> extends Error {
  config: AxiosRequestConfig;
  code?: string;
  request?: any;
  response?: AxiosResponse<T>;
  isAxiosError: boolean;
  toJSON: () => object;
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

テスト駆動開発でbuttonを追加

Vue.jsで作成した空白のwebページに、Jestを利用したテスト駆動開発でbuttonを追加をする。

  • button(id=sampleButton)の存在をテスト
  • buttonのラベル(サンプルのボタン)をテスト
    • buttonのラベルが変数にバインドされていない場合
    • buttonのラベルが変数にバインドされている場合
    • buttonのラベルのテスト
    • buttonのラベルがバインドされた変数のテスト
  • buttonがクリックされて呼び出されるメソッド(onClick)のテスト

完成したソースコードはGitHubのリポジトリにある。

buttonを追加する前のディレクトリ構成とファイル

buttonを追加する前のディレクトリ構成

├ index.html
├ public
│  └ index.html
└ src
    ├ App.vue
    ├ components
    │ └ SampleButton.vue
    └ main.js
public/index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <noscript>
      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>
src/main.js
import Vue from "vue";
import App from "./App.vue";

Vue.config.productionTip = false;

new Vue({
  render: h => h(App)
}).$mount("#app");
src/App.vue
<template>
  <div id="app">
    <SampleButton />
  </div>
</template>

<script>
import SampleButton from "./components/SampleButton.vue";

export default {
  name: "App",
  components: {
    SampleButton
  }
};
</script>

<style></style>
src/components/SampleButton.vue
<template>
  <div></div>
</template>

<script>
export default {
  name: "SampleButton"
};
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped></style>

buttonの存在を確認するテストを追加

buttonを追加するために、buttonの存在を確認するテストを追加する。

tests/unit/button.spec.js
import { shallowMount } from '@vue/test-utils'
import SampleButton from '@/components/SampleButton.vue'

describe('追加するbuttonのidをsampleButtonとする。', () => {
  let wrapper
  beforeEach(() => {
    wrapper = shallowMount(SampleButton)
  })

  it('存在する。', () => {
    expect(wrapper.find('#sampleButton').exists()).toBeTruthy()
  })
})

まだbuttonは追加していないので、以下のようにエラーになる。

FAIL  tests/unit/button.spec.js
  追加するbuttonのidをsampleButtonとする。
    ✕ 存在する。 (39ms)

  ● 追加するbuttonのidをsampleButtonとする。 › 存在する。

    expect(received).toBeTruthy()

    Received: false

       9 |
      10 |   it('存在する。', () => {
    > 11 |     expect(wrapper.find('#sampleButton').exists()).toBeTruthy()
         |                                                    ^
      12 |   })
      13 | })
      14 |

src/components/SampleButton.vueにbuttonを追加することで、テストは成功する。

src/components/SampleButton.vue(抜粋)
<template>
  <div>
   <button id='sampleButton'></button>
  </div>
</template>

buttonのラベルのテストを追加

buttonのラベルを確認するテストを追加する。

tests/unit/button.spec.js(抜粋)
  it('ラベル(サンプルのラベル)が正しい。', () => {
    expect(wrapper.find('#sampleButton').text()).toBe('サンプルのラベル')
  })

ラベルを設定していないので、以下のようにエラーになる。

FAIL  tests/unit/button.spec.js
  追加するbuttonのidをsampleButtonとする。
    ✓ 存在する。 (23ms)
    ✕ ラベル(サンプルのラベル)が正しい。 (6ms)

  ● 追加するbuttonのidをsampleButtonとする。 › ラベル(サンプルのラベル)が正しい。

    expect(received).toBe(expected) // Object.is equality

    Expected: "サンプルのラベル"
    Received: ""

      13 |
      14 |   it('ラベル(サンプルのラベル)が正しい。', () => {
    > 15 |     expect(wrapper.find('#sampleButton').text()).toBe('サンプルのラベル')
         |                                                  ^
      16 |   })
      17 | })
      18 |

buttonのラベルをtemplateのHTMLに書いてしまう方法と、ラベルをバインドした変数にJavaScriptで設定する方法がある。

buttonのラベルを変数にバインドしない場合

ますは、templateのHTMLに書いてしまう場合には、src/components/SampleButton.vueのtemplateを修正することでテストは成功する。

src/components/SampleButton.vue(抜粋)
<template>
  <div>
   <button id='sampleButton'>サンプルのラベル</button>
  </div>
</template>

buttonのラベルを変数にバインドする場合

buttonのラベルのテストを追加

ラベルをバインドした変数にJavaScriptで設定する場合は、src/components/SampleButton.vueのtemplateとscriptを修正する。

src/components/SampleButton.vue(抜粋)
<template>
  <div>
    <button id="sampleButton">{{ sampleLabel }}</button>
  </div>
</template>

<script>
export default {
  name: "SampleButton",
  data() {
    return {
      sampleLabel: "サンプルのラベル"
    };
  },
  methods: {
    onClick() {
      return;
    }
  }
};
</script>

buttonのラベルがバインドされた変数のテストを追加

buttonのラベルがバインドされた変数(sampleLabel)のテストを追加する。

tests/unit/button.spec.js
it('buttonにラベルにバインドされた変数が正しい。', () => {
    expect(wrapper.vm.sampleLabel).toBe('サンプルのラベル')
  })

既に正しく変数にバインドされているため、テストは成功する。

buttonがクリックされて呼び出されるメソッドのテスト

buttonがクリックされるとonClickが呼び出されることを確認するテストを追加する。

tests/unit/button.spec.js
  it('クリックするとonClickが呼び出される。', () => {
    const onClick = jest.fn()
    wrapper.setMethods({ onClick })
    wrapper.find('#sampleButton').trigger('click')
    expect(onclick).toHaveBeenCalledTimes(1)
  })

onClickを呼び出すように設定していないため、以下のようにエラーになる。

FAIL  tests/unit/button.spec.js
  追加するbuttonのidをsampleButtonとする。
    ✓ 存在する。 (22ms)
    ✓ ラベル(サンプルのラベル)が正しい。 (2ms)
    ✕ クリックするとonClickが呼び出される。 (7ms)

  ● 追加するbuttonのidをsampleButtonとする。 › クリックするとonClickが呼び出される。

    TypeError: Cannot set property 'onClick' of undefined

      18 |   it('クリックするとonClickが呼び出される。', () => {
      19 |     const onClick = jest.fn()
    > 20 |     wrapper.setMethods({ onClick })
         |             ^
      21 |     wrapper.find('#sampleButton').trigger('click')
      22 |     expect(onclick).toHaveBeenCalledTimes(1)
      23 |   })

src/components/SampleButton.vueを以下のように修正して、クリックされたときにonCkickを呼び出すように設定することでテストに成功する。

src/components/SampleButton.vue(抜粋)
<template>
  <div>
   <button id='sampleButton' @click="onClick">サンプルのラベル</button>
  </div>
</template>

<script>
export default {
  name: "SampleButton",
  methods: {
    onClick() {
      return
    }
  }
};
</script>

以上で、Vue.jsで作成するwebページにテスト駆動でbuttonを追加した。

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

prototype.js IE7でセレクタでのDOM取得ができない場合の対処法

現象

prototype.js(1.7)にてDOM取得を行おうとした際、
IE11(7互換モード)にてjavascriptエラーが発生した。

具体的には、formタグ内のhiddenをCSSセレクタにて取得しようとしてエラーになった。

前提

・prototype.js ver.1.7
・1ページ内に同じidの要素が複数ある
 (本当はidが重複してはいけないんだけど、大人の事情で回避できず)

書いたコード

html
<form name="form">
    <input type="hidden" id="hidden">
</form>
js
var submitElem = $$('form[name=form] #hidden')[0];
submitElem.value = 'execute';

IEから吐かれたエラー

未定義または NULL 参照のプロパティ 'value' は設定できません
jsの1行目でエレメントが取れていないので、2行目でnull参照となっている

対処法

#hidden → input[id=hidden] に変更することで解決する。

js
var submitElem = $$('form[name=form] input[id=hidden]')[0];
submitElem.value = 'execute';

これでhiddenのvalueに値が入った。

備考

document.getElementsBySelectorでも発生するか確認したが
IE7互換モードにおいては、上記functionはそもそも非対応であった。

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

React Reduxには今後Redux Toolkitも使うのがいいと思う

ReactをTypeScriptで始めることが随分楽になったと感じています。Create React Appの以下のコマンドでOKです。

$ npx create-react-app my-app --template typescript

https://create-react-app.dev/docs/adding-typescript/#installation

2020年2月の中頃からReduxのテンプレートが利用可能となり、以下のコマンドで始めることができます。

$ npx create-react-app my-app --template redux
# or 
$ npx create-react-app my-app --template redux-typescript

https://github.com/reduxjs/cra-template-redux

このReduxテンプレートにはRedux Toolkitが使われています。https://redux-toolkit.js.org/
本格的に利用していくのはこれからですが、使わない場合と比べてここが便利だ(そうだ)というあたりを紹介します。

利点

初期設定が簡単

次のmiddlewareが設定済みとなります。

  • Redux Thunk
  • immutable-state-invariant
  • serializable-state-invariant-middleware

Redux Thunkは非同期通信のためのライブラリで、他2つは開発用のmiddlewareでReduxで禁止しているstateの変更を検知してくれます。

また、Redux Devtools Extensionも有効になっています。
これらを自分で適用しようとすると少々面倒です。

createSliceでActionのコードは不要に

公式ドキュメントからの抜粋となります

const counterSlice = createSlice({
  name: 'counter',
  initialState: 0,
  reducers: {
    increment: state => state + 1,
    decrement: state => state - 1
  }
})

const store = configureStore({
  reducer: counterSlice.reducer
})

const { actions, reducer } = counterSlice
const { increment, decrement } = actions

こんな感じでAction, Reducerを定義できます。これによりほぼActionのコードがなくなるのではと期待しています。今まではComponent, Action, Reducerに対してロジックが多少入ることが気になっていましたが、Component, Reducerでそれぞれの責務でロジックを実装できそうです :smiley:

Reducerのネストはいつも通りな感じです。

const sliceA = createSlice({
  name: "reducerA",
  //...
})
const sliceB = createSlice({
  name: "reducerB",
  //...
})

import { combineReducers } from "@reduxjs/toolkit"
const mergeReducer = combineReducers({
  reducerA: sliceA.reducer,
  reducerB: sliceB.reducer,
})

非同期通信にはRedux Thunkが採用

const createUser = createAsyncThunk(
  'users/createUser',
  async () => {
    // API処理
  }
)

createAsyncThunkでは次の3つのActionを生成してくれます。

  • pending
  • fulfilled
  • rejected

sliceではextraReducersのbuilderを利用して追加します。createAsyncThunkで定義されたActionを利用するためです。

reducer.js
const userSlice = createSlice({
  name: "user",
  initialState: {
    user: null,
  },
  reducers: {},
  extraReducers: builder => {
    builder.addCase(createUser.pending, (state, action) => {})
    builder.addCase(createUser.fulfilled, (state, action) => {})
    builder.addCase(createUser.rejected, (state, action) => {})
  }
})

component.jsx
import { useDispatch } from "react-redux"

function UserComponent() {
  const dispatch = useDispatch()

  const handleSubmit = async () => {
    const resultAction = await dispatch(createUser())
    // createAsyncThunkで生成されるActionと比較して処理を変更できる
    if (createUser.fulfilled.match(resultAction)) {
      // success
    } else {
      // error
    }
  };
  return (
    <div>
      <form></form>
      <button onClick={handleSubmit}>submit</button>
    </div>
  )
}

やっておいた方がいいこと

RootReducerの型定義

TypeScriptで利用する際に用意しておいた方がいいです。以下で定義しておきます。

store.ts
import { combineReducers, configureStore } from "@reduxjs/toolkit"

export const rootReducer = combineReducers({
  moduleA: moduleAReducer,
})
export type RootReducerType = ReturnType<typeof RootReducer>

RootとなるReducerの型定義はあると便利ぐらいの気持ちです。

useDispatchのラップ

store.ts
import { configureStore } from "@reduxjs/toolkit"
const store = configureStore({
  reducer: rootReducer
})
export type AppDispatch = typeof store.dispatch
customHooks.ts
import { useDispatch } from "react-redux"
function useAppDispatch(): AppDispatch {
  return useDispatch<AppDispatch>()
}

createAsyncThunkで型定義のエラーを起こさないためです。

注意点

ImmerによるReducerのstateのImmutable

以下のようにstateを直接変更するようなコードを書いても問題ありません。Redux ToolkitではImmerを利用しており、実際にstateを変更しているわけではないとのことです。

interface CounterState {
  value: number;
}

const initialState: CounterState = {
  value: 0,
};

export const slice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment: state => {
      state.value += 1;
    }
  },
});

まとめ

煩雑な設定がなくなり、ActionやReducerの実装も統一できそうです。これからReduxを始める人、そうでない人にもおすすめです :smiley:

参考

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

Google Apps ScriptでLINE BOTのおうむ返し&スプレッドシートに自動入力

YouTubeで公開した動画Qiita記事の続きです。

YouTubeを観て実際に作ってみた人から、こんな質問がありました

例えば、

田中
ご飯
味噌汁
生卵

と入力したら、botが

田中
ご飯
味噌汁
生卵

を食べました。

と返す方法はありませんかね?

コード

コード.js
// LINE developersのメッセージ送受信設定に記載のアクセストークン
const LINE_TOKEN = 'アクセストークン'; // Messaging API設定の一番下で発行できるLINE Botのアクセストークン(Channel Secretはいらないみたいです。)
const LINE_URL = 'https://api.line.me/v2/bot/message/reply';

// postリクエストを受取ったときに発火する関数
function doPost(e) {

  // 応答用Tokenを取得
  const replyToken = JSON.parse(e.postData.contents).events[0].replyToken;
  // メッセージを取得
  const userMessage = JSON.parse(e.postData.contents).events[0].message.text;
  // メッセージを改行ごとに分割
  const all_msg = userMessage.split("\n");
  // 入力データの数を取得
  const msg_num = all_msg.length;

  // 返答用メッセージを作成
  const messages = [
    {
      'type': 'text',
      'text':  `${userMessage}\n\nを食べました。`,
    }
  ]

  // ***************************
  // スプレットシートからデータを抽出
  // ***************************
  // 1. 今開いている(紐付いている)スプレッドシートを定義
  const sheet     = SpreadsheetApp.getActiveSpreadsheet();
  // 2. ここでは、デフォルトの「シート1」の名前が書かれているシートを呼び出し
  const listSheet = sheet.getSheetByName("シート1");
  // 3. 最終列の列番号を取得
  const numColumn = listSheet.getLastColumn();
  // 4. 最終行の行番号を取得
  const numRow    = listSheet.getLastRow()-1;
  // 5. 範囲を指定(上、左、右、下)
  const topRange  = listSheet.getRange(1, 1, 1, numColumn);      // 一番上のオレンジ色の部分の範囲を指定
  const dataRange = listSheet.getRange(2, 1, numRow, numColumn); // データの部分の範囲を指定
  // 6. 値を取得
  const topData   = topRange.getValues();  // 一番上のオレンジ色の部分の範囲の値を取得
  const data      = dataRange.getValues(); // データの部分の範囲の値を取得
  const dataNum   = data.length +2;        // 新しくデータを入れたいセルの列の番号を取得

  // ***************************
  // スプレッドシートにデータを入力
  // ***************************
  // 最終列の番号まで、順番にスプレッドシートの左からデータを新しく入力
  for (let i = 0; i < msg_num; i++) {
    SpreadsheetApp.getActiveSheet().getRange(dataNum, i+1).setValue(all_msg[i]);
  }

  const after_msg = {
    'type': 'text',
    'text': "保存完了。明日もしっかり食べよう!",
  }
  messages.push(after_msg);

  // lineで返答する
  UrlFetchApp.fetch(LINE_URL, {
    'headers': {
      'Content-Type': 'application/json; charset=UTF-8',
      'Authorization': `Bearer ${LINE_TOKEN}`,
    },
    'method': 'post',
    'payload': JSON.stringify({
      'replyToken': replyToken,
      'messages': messages,
    }),
  });

  ContentService.createTextOutput(JSON.stringify({'content': 'post ok'})).setMimeType(ContentService.MimeType.JSON);

}

解説

入力データは userMessageに入っているので、そのまま最初の返答に使っています。
基本的にJavaScriptの知識で自由にカスタマイズできるので、やってみてください!

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

Jest+CircleCIなプロジェクトにCodeCov(カバレッジレポート)を導入するまでの手順ハンズオン

概要

  • テストのコードカバレッジのレポートにCodeCovを使いバッジimage.pngをゲットするまでのハンズオンメモです

開発環境と構成

  • 開発環境
開発言語 JavaScript(ES6)/Node.js
テストフレームワーク Jest
Gitホスティング GitHub
CIツール CircleCI
カバレッジレポート CodeCov
  • 構成
    全体としてはざっくり以下のような構成となります

image.png

CodeCovのサインアップとプロジェクト設定

  1. codecovを開く

まず、CodeCovのサインアップから。

image.png

2.プロジェクトはGithubにホストしているのでGithubを選択

image.png

3.Githubへのアクセス認可の画面がでるので、Authorize CodeDevをクリック

image.png

4.CodeCovの初期画面がでる。

リポジトリがまだ登録されていないのでAdd Repositoryをクリック。

image.png

5.Choose a new repository belowという画面がでてレポジトリ一覧が表示される。

「ここで一覧から対象のリポジトリを選択すれば良し」と思ったら、、、すべてのリポジトリが表示されていない模様

image.png

6.直接リポジトリを指定する

image.png

↑のようなメッセージがでていたので、直接指定することにする。

https://github.com/riversun/event-listener-helperというリポジトリを設定したかったので、上のフォーマットにならい、https://codecov.io/gh/riversun/event-listener-helperにアクセスしたら、そのプロジェクトの初期画面が表示される。

image.png

7.CodeCovのトークンをメモする

初期画面に表示されているトークンをメモしておく。
あとでCircleCIの環境変数に登録する。

環境変数名はCODECOV_TOKENとなる。

CircleCI用のconfig.ymlにCodeCov関係の設定を追記する

CodeCov用のOrbがあるので、その利用を前提としてconfig.ymlを編集する

ちなみに、Orbsとは

Orbs とは
CircleCI Orbs は、ジョブ、コマンド、Executor のような設定要素をまとめた共有可能なパッケージです。 CircleCI 独自の認証済み Orbs のほか、パートナー企業によるサードパーティ製 Orbs を用意しています。

(出典:https://circleci.com/docs/ja/2.0/orb-intro/)

config.ymlを以下のようにした

version: 2.1
orbs:
  node: circleci/node@1.1.6
  codecov: codecov/codecov@1.0.5
jobs:
  build-and-test:
    executor:
      name: node/default
    steps:
      - checkout
      - node/with-cache:
          steps:
            - run: npm install
            - run: npm test
            - codecov/upload:
                file: ./coverage/lcov.info
workflows:
  build-and-test:
    jobs:
      - build-and-test

ポイント

  • orbscodecov: codecov/codecov@1.0.5を追加する

  • カバレッジレポートファイルのアップロードをするためのコマンドcodecov/uploadを以下のように追記する。

config.yml(抜粋)
- codecov/upload:
    file: ./coverage/lcov.info
  • 実際なにやってるか
    • codecov/uploadの内部処理ではcurlコマンドでカバレッジレポートファイルlcov.infoCircleCI(docker)→CodeCovに送信している
    • lcov.info(カバレッジレポートが格納されたファイル)は[root]/coverage/lcov.infoに生成される前提。そのように生成されるようにJestの設定(jest.config.js)を次でする

Jestの設定

念のためにJestの設定をみておく。

やりたいことは以下の2点

  • Jestでテストするときにコードカバレッジも計測するようにすること
  • コードカバレッジのレポートが目的の場所に生成されるようにすること

ということでjest.config.jsの設定をまるっと掲載すると以下のようになる

jest.config.js
module.exports = {
  verbose: true,
  testEnvironment: 'jsdom',
  testMatch: [
    "**/test/**/*.test.js"
  ],
  testTimeout: 5000,
  moduleDirectories: [
    "node_modules"
  ],
  transformIgnorePatterns: [
    "node_modules/(?!(@riversun/event-emitter)/)"
  ],
  "coverageDirectory": "./coverage/",
  "collectCoverage": true
};

本稿で重要なところは以下の部分

jest.config.js(抜粋)
 "coverageDirectory": "./coverage/",
  "collectCoverage": true

coverageDirectoryでカバレッジ計測レポートの関連ファイルの生成先を指定する
collectCoverage:trueでテストする毎にカバレッジ計測レポートが生成されるようになる
(jestコマンドに --coverageオプションをつけて、必要なときに生成するアプローチでもアリ)

CircleCIでプロジェクトをセットアップする

さてここからCircleCIでプロジェクトのセットアップをしていく。

1. https://circleci.com/ を開いてGo to Appクリック(CircleCI自体のサインアップは割愛)

image.png

2.サードパーティ製のOrbsをつかえるようにする

さきほど、CodeCovOrbを使う記述をconfig.ymlに記述したが、サードパーティ製のOrbsを使うための設定を1度だけやっておく必要がある。

画面左の歯車アイコンをクリックすると、

image.png

Organization Settingsという画面がでるので、Allow uncertified orbsYesにすることで、サードパーティ製Orbsの使用を許可できる。

image.png

この設定はプロジェクト横断設定なので1度だけやればOK。

3.プロジェクトを追加する

さて、サードパーティ製のOrbsの許可までできたので、ここでCIしたいプロジェクトを登録する。

以下の画面で、Add projectをクリック

image.png

↑の画面じゃなくて、↓の画面の場合もある(↑のほうは新UIのプレビュー版で↓は現行UIという位置づけかな?いずれにせよAdd projectをクリックする。)

image.png

自分のプロジェクト一覧からCircleCIしたいプロジェクトのところにあるSet Up Projectをクリック

image.png

3.CircleCI用のConfigを追加する

ここでは、CircleCIが「自動でConfig作りましょうか?」とたずねてくれるが、自前のconfig.ymlを作ってPushしてあるので、Add Manuallyをクリック。

image.png

4.さっそくビルド

既に「circleci/config.ymlがあるならビルド開始できますよ」というメッセージが出るのでStart Buildingをクリックする。

image.png

5.予定通りビルド失敗

そして、CircleCI上にて、ビルドが失敗する。

image.png

CodeCovの環境変数を設定していないので最初のビルドは失敗した。

6.CodeCov用の環境変数を設定する

さきほどメモしたCodeCovの環境変数を、CircleCIにセットする。

現行UIでは以下のようにプロジェクト名の右にある歯車アイコンをクリックする

image.png

Environment Variablesを選択して、Add variableをクリック

image.png

ここで環境変数をセットできるので、環境変数名としてNameCODECOV_TOKENをセット、ValueにはさきほどCodeCovから取得した値をセットしてAdd Variable*をクリック

image.png

無事に環境変数をセットできた模様

image.png

ちなみに、
CircleCIの新UI(プレビュー版)のほうだと、同様の操作は以下のようになる。

対象のプロジェクトのPipelines画面で、Project Settingsをクリック

image.png

Environemnt Variablesを選択して、Add Variablesをクリックする

image.png

(あ、なるほど。UIレイアウト、デザインは違うけどラベルが一緒なので文字だけのドキュメンテーションにしたら同じ説明で済むようにできてますね)

CircleCIでビルドして、カバレッジレポートをCodeCovでみる

1.CircleCIでビルドする

ここまでで、CircleCIでビルドする準備ができたので、あとはコミット・プッシュするなり、ReRunするなりしてCircleCIを回すだけ。

以下のようにビルドが無事成功した模様。

image.png

2.CodeCovでカバレッジレポートを確認する

CodeCovを開き https://codecov.io/gh
さきほど登録したプロジェクトをクリック

image.png

カバレッジレポートが表示された

image.png

Readme.mdにカバレッジ計測バッジをはりつける

CodeCovのプロジェクト画面でSettingsタブでBadgeを選択すると、以下のようにバッジ用のコードがあるので、GithubのReadmeにバッジをつけたいときはMarkdownの内容をコピーして、それをReadme.mdにペーストすればできあがり。

image.png

ReadmeにCodeCovのバッジimage.pngが無事表示された
(本プロジェクトはこちら)

image.png

まとめ

  • 以下のようなJest,CircleCI,CodeCovの組み合わせの導入手順をご紹介しました

image.png

  • 便利でMotivationalなクラウドサービス群に感謝感謝!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScript で絵文字の文字数をカウントしたかった

JavaScript での絵文字を含む String の文字数カウントは結構ハードだった話。

問題の事象

? は直感的には1文字カウントされてほしいが、 length プロパティを参照すると以下のような結果になる。

'hoge'.length  // 4
'ほげ'.length  // 2
'?'.length  // 2 ...!?

なぜ?

length は単に文字数を返しているわけではない
JavaScript の内部では文字列を UTF-16 形式で保持しており、 lentgh はこの単位のコード数を返している。
ASCII やひらがなは1つの 16 bit で表されるが、絵文字の多くは サロゲートペア を使って2つの 16 bit で表現される。

前述の例は UTF-16 で以下のように表現される。

String.fromCharCode('0x0068', '0x006F', '0x0067', '0x0065')  // hoge
String.fromCharCode('0x307B', '0x3052')  // ほげ
String.fromCharCode('0xD83D', '0xDE07')  // ?

見ての通り ? はコード2つ分なので、 length プロパティの値は2となる。

'?'.length を1文字としてカウントするには

スプレッド構文 を使って Array に変換した上で length を取れば1文字(1要素)扱いされる。

[...'?'].length  // 1

これで解決?

文字コードの世界は広かった。
man shrugging と呼ばれる以下の絵文字は次のようにカウントされる。

'?‍♂️'.length  // 5 ...!?
[...'?‍♂️'].length  // 4 ...!?

この絵文字は ? ( 0xD83E 0xDD37 )と ♂ ( 0x2642 )の組み合わせであり、内部的には以下のように表現される。

String.fromCharCode('0xD83E', '0xDD37', '0x200D', '0x2642', '0xFE0F')

0x200D は絵文字の結合用のコード、 0xFE0F絵文字バリエーション・シーケンスと呼ばれるプレーン文字と絵文字を識別するためのコードらしい。
? は1文字として判定されるものの、後続のコードがそれぞれ1文字判定され、結果4となる。

これらの文字に関してはライブラリを使って対応できるようだが、現状ではシンプルな対応方法はなさそう...。

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

【phpbb】javascriptファイルをテンプレートごとに読み込む

javascriptファイルを特定のテンプレートで読み込みたい

phpbb3.2.5を使用しています。
ソースを見ると</body>の直前でjqueryが読み込まれています。
該当ファイルは./styles/prosilver/template/overall_footer.htmlです。
このファイルは基本的にすべてのページのフッター部分で使われています。

で、そのファイルの下の方に{$SCRIPTS}という記述があります。

/styles/prosilver/template/overall_footer.html
<!-- EVENT overall_footer_after -->

<!-- IF S_PLUPLOAD --><!-- INCLUDE plupload.html --><!-- ENDIF -->
{$SCRIPTS}

<!-- EVENT overall_footer_body_after -->

</body>
</html>

テンプレートごとにjavascriptファイルを読み込む場合にここで表示されます。

テンプレート側の記述は以下のような感じで、<!-- INCLUDEJS -->タグ内にjavascriptファイルを記述します。

/styles/prosilver/template/sample.html
<!-- INCLUDEJS {PATH}/javascript/sample.js -->

そうすると実際に生成されるhtmlは以下のような感じになります。

sample.html
<script type="text/javascript" src="./../assets/javascript/jquery.min.js"></script> 
<script type="text/javascript" src="./../assets/javascript/core.js?assets_version=55"></script>
<script src="./../assets/javascript/sample.js?assets_version=55"></script>/* ←今回追加した部分 */
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

アワード系のWebサイトで見かける、ゆったりした慣性スクロールの実装

AwwwardsFWAに掲載されているサイトでよく見かける、ゆったりした慣性スクロール。

http://madies.mx
http://www.amandabraga.com
http://56k.studiovoila.com

見覚えがある人もいると思いますが、スクロールの反応が気持ち遅れて来る、自前の慣性スクロールっぽいやつです。これのやり方の参考が少なく実装に手間取ったので、実装例をまとめました。他にも色々なアプローチがありそうですが、参考のひとつとして。

最初に断っておきますが、かなり邪悪な実装なので基本的には非推奨です。ブラウザの本来の機能を上書きする実装の多くは、ユーザーの利益にはならないので、冷静に判断してください。

デモ
https://codepen.io/nishinoshake/debug/dyowbyr

採用する利点と欠点

明らかに欠点の方が多いので、利点が勝ることは稀だと思います。
採用する場合は、多くのユーザーに害があることを認めた上で、罪を背負う覚悟で。

利点

  • このスクロールに心地よさを感じる人がいるらしい

欠点

  • 慣れていない人にとっては違和感がある
  • スクロール系のライブラリと相性が悪い
  • 低スペックなマシンだと見るに耐えない
  • 邪悪な実装がサイトの寿命を縮める

原理

スクロールと同期して、transformで要素を滑らせる。以下、スクロールしたいエリアをコンテナと呼びます。

1. コンテナにposition:fixedを設定
2. bodyにコンテナの高さを設定
3. スクロール量をコンテナのtransformに少しづつ設定

1.のfixedだけだと、bodyの高さが無くなってスクロールができなくなり、スクロールバーも表示されません。スクロールバーが無いままのサイトや、独自でカスタムしている邪悪なサイトもありますが、これ以上は罪を重ねない方が良いでしょう。

bodyにコンテナの高さを設定すると、通常のスクロールイベントを拾えるので、transformとの同期も楽になります。mousewheelやtouchmoveイベントの実装は辛いので、こちらの方がまだマシです。

前にJavaScriptのイベントをたくさん見られるサイトを作ったので、イベントに不慣れな方はぜひ。

実装例

<div id="container">
  ここにコンテンツ
</div>
class MomentumScroll {
  constructor(selector) {
    this.container = document.querySelector(selector)
    this.scrollY = 0
    this.translateY = 0
    this.speed = 0.1
    this.rafId = null
    this.isActive = false

    this.scrollHandler = this.scroll.bind(this)
    this.resizeHandler = this.resize.bind(this)

    this.run()
  }

  run() {
    if (this.isActive) {
      return
    }

    this.isActive = true

    this.on()
    this.setStyles()
  }

  destroy() {
    if (!this.isActive) {
      return
    }

    this.isActive = false

    this.off()
    this.clearStyles()

    if (this.rafId) {
      cancelAnimationFrame(this.rafId)
    }

    this.rafId = null
  }

  resize() {
    document.body.style.height = `${this.container.clientHeight}px`
  }

  scroll() {
    this.scrollY = window.scrollY || window.pageYOffset

    if (!this.rafId) {
      this.container.style.willChange = 'transform'
      this.rafId = requestAnimationFrame(() => this.render())
    }
  }

  on() {
    this.resize()
    this.scroll()
    window.addEventListener('scroll', this.scrollHandler, { passive: true })
    window.addEventListener('resize', this.resizeHandler)
    window.addEventListener('load', this.resizeHandler)
  }

  off() {
    window.removeEventListener('scroll', this.scrollHandler)
    window.removeEventListener('resize', this.resizeHandler)
    window.removeEventListener('load', this.resizeHandler)
  }

  setStyles() {
    this.container.style.position = 'fixed'
    this.container.style.width = '100%'
    this.container.style.top = 0
    this.container.style.left = 0
  }

  clearStyles() {
    document.body.style.height = ''
    this.container.style.position = ''
    this.container.style.width = ''
    this.container.style.top = ''
    this.container.style.left = ''
    this.container.style.transform = ''
    this.container.style.willChange = ''
  }

  render() {
    const nextY = this.translateY + (this.scrollY - this.translateY) * this.speed    
    const isNear = Math.abs(this.scrollY - nextY) <= 0.1

    this.translateY = isNear ? this.scrollY : nextY

    const roundedY = Math.round(this.translateY * 100) / 100

    this.container.style.transform = `translate3d(0, -${roundedY}px, 0)`

    if (isNear) {
      this.rafId = null
      this.container.style.willChange = ''
    } else {
      this.rafId = requestAnimationFrame(() => this.render())
    }
  }
}

document.addEventListener('DOMContentLoaded', () => {
  const momentumScroll = new MomentumScroll('#container')

  // document.body.addEventListener('click', () => {
  //   if (momentumScroll.isActive) {
  //     momentumScroll.destroy() 
  //   } else {
  //     momentumScroll.run() 
  //   }
  // })
})

Codepen

See the Pen Minimal Momentum Scroll by nishinoshake (@nishinoshake) on CodePen.

スマホを除外したい

iPhoneやAndroidで実行したくない場合は、ユーザーエージェントを確認して、PCのブラウザの時だけ実行してください。

ゆったりさせるところの抜粋

スピードをパラメータで設定して、段階的に適用していくイメージ。

// 必要なところだけ
this.scrollY = 0
this.translateY = 0
this.speed = 0.1

// スクロール時に値を設定
this.scrollY = window.scrollY || window.pageYOffset

// レンダリング時に少しづつ実際のスクロール値に近づける
const nextY = this.translateY + (this.scrollY - this.translateY) * this.speed

他に良い実装があったら知りたい

できる限りの最適化はしているつもりですが、良い実装があったら教えて下さい!

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

Select2のtagsの日本語入力のバグ解消方法

公式サイトもバグってる

スクリーンショット 2020-03-30 8.35.42.png

公式サイト

原因はバージョン

原因はバージョンなので、バージョンをダウングレードしましょう。調査したら4.0.2まではバグが発生しないので、バグが発生しない最新バージョンの4.0.2を使いましょう!

<!doctype html>
<html lang="ja">
  <head>
    <!-- Select2 -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.2/js/select2.min.js"></script>
  </head>
  <body>
  </body>
</html>

スクリーンショット 2020-03-30 8.29.09.png

参考記事:select2 v4で日本語対応してtagsのバグを回避した方法(根治ではない

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

Threejsで筒を作ってみる

はじめに

Three.jsで筒を作りたいなと思い、挑戦しました。
結果、作ったのは↓の2種類

image.png

1.CylinderGeometryで筒を作ってみた

1-1.参考

1-2.スクリプト

CylinderGeometryで筒.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8"/>
  <style>
      body {
          margin: 0;
      }
  </style>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/110/three.min.js"></script>

  <script>
    // ページの読み込みを待つ
    window.addEventListener('load', init);

    function init() {

      // サイズを指定
      const width = 960;
      const height = 540;

      // レンダラーを作成
      const renderer = new THREE.WebGLRenderer(
        {
          canvas: document.querySelector('#myCanvas'),
          alpha: true
        }
      );
      renderer.setPixelRatio(window.devicePixelRatio);
      renderer.setSize(width, height);
      renderer.setClearColor( 0x000000, 0 ); // クリアカラーで消す、2つめの引数が0

      // シーンを作成
      const scene = new THREE.Scene();

      // カメラを作成
      const camera = new THREE.PerspectiveCamera(45, width / height);
      camera.position.set(0, 0, +1000);


      // ↓ 筒 -----------------------------------------------------------
      const loader = new THREE.TextureLoader();
      loader.load(
        './tex001.png',
        function(texture){
          // 円柱
          const cylinderMaterial01 = new THREE.MeshBasicMaterial(
            {
              map:texture,
              side: THREE.DoubleSide
            }
          );
          // ↑ side について
          // THREE.FrontSide :表面だけ描画
          // THREE.BackSide  :裏面だけを描画
          // THREE.DoubleSide:両面を描画

          let radiusTop = 250; //上面の半径です。0にすると円錐になります。
          let radiusBottom = 250; //底面の半径です。
          let objHeight = 400; //高さです。
          let radiusSegments = 32; //円周の分割数です。
          let heightSegments = 32; //高さの分割数です。
          //let openEnded = false; //true:フタをしない,false:フタをする
          let openEnded = true; //true:フタをしない,false:フタをする

          const cylinder01 = new THREE.Mesh( 
              //円柱のジオメトリー(上面半径,下面半径,高さ,円周分割数)
              new THREE.CylinderGeometry(radiusTop, radiusBottom, objHeight, radiusSegments, heightSegments, openEnded),
              cylinderMaterial01
          );

          cylinder01.position.set(2, 0, 0); //(x,y,z)
          scene.add(cylinder01);

          tick();

          // 毎フレーム時に実行されるループイベントです
          function tick() {
            cylinder01.rotation.y += 0.01;
            cylinder01.rotation.x += 0.015;
            cylinder01.rotation.z += 0.02;
            renderer.render(scene, camera); // レンダリング
            requestAnimationFrame(tick);
          }
        }
      );
      // ↑ 筒 -----------------------------------------------------------

      //環境光源(アンビエントライト):すべてを均等に照らす、影のない、全体を明るくするライト
      const ambient = new THREE.AmbientLight(0xFFFFFF, 0.9);
      scene.add(ambient); //シーンにアンビエントライトを追加

      //平行光
      const light = new THREE.DirectionalLight(0xFFFFFF, 1);
      light.position.set(200,200,300);
      scene.add(light);
    }
  </script>
</head>
<body>
  <canvas id="myCanvas"></canvas>
</body>
</html>

1-3.ポイント

ポイントは2つ

  • CylinderGeometryのフタを外す
//let openEnded = false; //true:フタをしない,false:フタをする
let openEnded = true; //true:フタをしない,false:フタをする

//円柱のジオメトリー(上面半径,下面半径,高さ,円周分割数)
new THREE.CylinderGeometry(radiusTop, radiusBottom, objHeight, radiusSegments, heightSegments, openEnded)
  • 両面にテクスチャを適用する
const cylinderMaterial01 = new THREE.MeshBasicMaterial(
{
    map:texture,
    side: THREE.DoubleSide
}
// ↑ side について
// THREE.FrontSide :表面だけ描画
// THREE.BackSide  :裏面だけを描画
// THREE.DoubleSide:両面を描画

2.Geometryクラスで筒を作ってみた

2-1.参考

【three.js】Geometryクラスで正三角柱をつくる
https://qiita.com/baikichiz/items/4d8cf1a4f0f58d986152

2-2.各点の番号

配列 vertices に格納した順番で番号が振られる。(面を作成するときに使用)

2-3.スクリプト

Geometryクラスで筒.html
<!DOCTYPE html>

<html>
<head>
    <meta charset=utf-8>
    <title>My first three.js app</title>
    <style>
        body { margin: 0; }
        canvas { width: 100%; height: 100% }
    </style>
</head>
<body>

<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/105/three.min.js"></script>
<script>
// 三角柱をつくる。原点は底面正三角形の重心、柱の高さの半分の位置とする。
// length:
// height:
// depth:
function createTriangle(argWidth, argHeight, argDepth)
{

    var faceColor = 0x0000FF;
    var hW = argWidth / 2.0;
    var hH = argHeight/ 2.0;
    var hD = argDepth;

    var rate = 0.1
    var sW = argWidth / 2.0 - argWidth * rate;
    var sH = argHeight/ 2.0 - argHeight * rate;


    var vertices = [
        new THREE.Vector3(-hW,     0, -hH), // 0
        new THREE.Vector3( hW,     0, -hH), // 1
        new THREE.Vector3( hW,     0,  hH), // 2
        new THREE.Vector3(-hW,     0,  hH), // 3
        new THREE.Vector3(-hW,   -hD, -hH), // 4
        new THREE.Vector3( hW,   -hD, -hH), // 5
        new THREE.Vector3( hW,   -hD,  hH), // 6
        new THREE.Vector3(-hW,   -hD,  hH), // 7

        new THREE.Vector3(-sW,     0, -sH), // 8
        new THREE.Vector3( sW,     0, -sH), // 9
        new THREE.Vector3( sW,     0,  sH), // 10
        new THREE.Vector3(-sW,     0,  sH), // 11

        new THREE.Vector3(-sW,   -hD, -sH), // 11
        new THREE.Vector3( sW,   -hD, -sH), // 12
        new THREE.Vector3( sW,   -hD,  sH), // 13
        new THREE.Vector3(-sW,   -hD,  sH), // 14
    ];
    var faces = [
        // ↓ 上部 ↓ ----
        new THREE.Face3(0, 3, 8),
        new THREE.Face3(3, 11, 8),
        new THREE.Face3(3, 2, 11),
        new THREE.Face3(2, 10, 11),
        new THREE.Face3(2, 1, 10),
        new THREE.Face3(10, 1, 9),
        new THREE.Face3(1, 0, 9),
        new THREE.Face3(9, 0, 8),

        // ↑ 上部 ↑ ----
        new THREE.Face3(1, 2, 5),
        new THREE.Face3(6, 5, 2),
        new THREE.Face3(6, 2, 3),
        new THREE.Face3(7, 6, 3),
        new THREE.Face3(7, 3, 0),
        new THREE.Face3(4, 7, 0),
        new THREE.Face3(0, 1, 5),
        new THREE.Face3(0, 5, 4),
        // ↓ 下部 ↓ ----
        new THREE.Face3(4, 12, 7),
        new THREE.Face3(12,15, 7),
        new THREE.Face3(7, 15, 14),
        new THREE.Face3(7, 14, 6),
        new THREE.Face3(14, 5, 6),
        new THREE.Face3(14, 13, 5),
        new THREE.Face3(13, 4, 5),
        new THREE.Face3(13, 12, 4),
        // ↑ 下部 ↑ ----
    // 内側
        new THREE.Face3(13, 9, 8),
        new THREE.Face3(12, 13, 8),
        new THREE.Face3(8, 11, 12),
        new THREE.Face3(15, 12, 11),
        new THREE.Face3(11, 10, 14),
        new THREE.Face3(11, 14, 15),
        new THREE.Face3(13, 10, 9),
        new THREE.Face3(13, 14, 10),
        new THREE.Face3(8, 11, 12),
        new THREE.Face3(15, 12, 11),
    ];

    var geometry = new THREE.Geometry();
    var i = 0;
    for (i = 0; i < vertices.length; i++) {
        geometry.vertices.push(vertices[i]);
    }
    for (i = 0; i < faces.length; i++) {
        geometry.faces.push(faces[i]);
    }

    var material = new THREE.MeshBasicMaterial({ color: faceColor });

    // 三角柱のワイヤーフレームを描く
    var wireframeGeometry = new THREE.EdgesGeometry(geometry);
    var wireframeMaterial = new THREE.LineBasicMaterial({ color: 0xFFFFFF, linewidth: 4 });

    var triangleMesh = new THREE.Mesh(geometry, material);
    var wireframe = new THREE.LineSegments(wireframeGeometry, wireframeMaterial);

    triangleMesh.add(wireframe);
    return triangleMesh;
}

// x, y, z軸を赤、緑、青で描く
// length: 軸の長さ
function createAxes(length)
{
    var createOneAxis = function (color, vertex) {
        var material = new THREE.LineBasicMaterial({
            color: color
        });
        var vertices = [
            new THREE.Vector3(10, -10, -10),
            vertex
        ];
        var geometry = new THREE.Geometry();
        geometry.vertices = vertices;

        var line = new THREE.Line(geometry, material);
        return line;
    };

    return [
        createOneAxis(0x770000, new THREE.Vector3(length, 0, 0)),
        createOneAxis(0x007700, new THREE.Vector3(0, length, 0)),
        createOneAxis(0x000077, new THREE.Vector3(0, 0, length))
    ];
}


   window.onload = function() {

    var scene = new THREE.Scene();
    var camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
    var renderer = new THREE.WebGLRenderer();
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);

    var mesh = createTriangle(20, 20, 20);  scene.add(mesh);
    var axes = createAxes(100); axes.forEach(function (a) { scene.add(a);})
    //----------------------------//
    var hW = 10;
    var hH = 10;
    var hD = 20;

        var rate = 0.1
        var sW = hW - hW * 2 * rate;
        var sH = hH - hW * 2 * rate;

    //----------------------------//

    camera.position.z = 30;
    camera.position.y = 30;
    camera.position.x = 15;
    camera.lookAt(0, 0, 0);

    function animate() {
        mesh.rotation.x += 0.01;
        mesh.rotation.y += 0.01;
        requestAnimationFrame(animate);
        renderer.render(scene, camera);
    }

    /////-- ↓ マウスイベント ↓ --//////
    var mousedown = false;
    renderer.domElement.addEventListener('mousedown', function(e) {
            mousedown = true;
    }, false);

    renderer.domElement.addEventListener('mousemove', function(e) {
        if (!mousedown) return;
        mesh.rotation.x += 0.1;
        mesh.rotation.y += 0.1;
        render();
    }, false);

    renderer.domElement.addEventListener('mouseup', function(e) {
            mousedown = false;
    }, false);

    function render(){
            renderer.render(scene, camera);
    }
    /////-- ↑ マウスイベント ↑ --//////
    animate();
   }
</script>
</body>
</html>

おわりに

Geometryクラスで筒を作るのは、面倒な上に筒っぽさの完成度が低い。

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

Three.jsで筒を作ってみる

はじめに

Three.jsで筒を作りたいなと思い、挑戦しました。
結果、作ったのは↓の2種類

image.png

1.CylinderGeometryで筒を作ってみた

1-1.参考

1-2.スクリプト

CylinderGeometryで筒.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8"/>
  <style>
      body {
          margin: 0;
      }
  </style>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/110/three.min.js"></script>

  <script>
    // ページの読み込みを待つ
    window.addEventListener('load', init);

    function init() {

      // サイズを指定
      const width = 960;
      const height = 540;

      // レンダラーを作成
      const renderer = new THREE.WebGLRenderer(
        {
          canvas: document.querySelector('#myCanvas'),
          alpha: true
        }
      );
      renderer.setPixelRatio(window.devicePixelRatio);
      renderer.setSize(width, height);
      renderer.setClearColor( 0x000000, 0 ); // クリアカラーで消す、2つめの引数が0

      // シーンを作成
      const scene = new THREE.Scene();

      // カメラを作成
      const camera = new THREE.PerspectiveCamera(45, width / height);
      camera.position.set(0, 0, +1000);


      // ↓ 筒 -----------------------------------------------------------
      const loader = new THREE.TextureLoader();
      loader.load(
        './tex001.png',
        function(texture){
          // 円柱
          const cylinderMaterial01 = new THREE.MeshBasicMaterial(
            {
              map:texture,
              side: THREE.DoubleSide
            }
          );
          // ↑ side について
          // THREE.FrontSide :表面だけ描画
          // THREE.BackSide  :裏面だけを描画
          // THREE.DoubleSide:両面を描画

          let radiusTop = 250; //上面の半径です。0にすると円錐になります。
          let radiusBottom = 250; //底面の半径です。
          let objHeight = 400; //高さです。
          let radiusSegments = 32; //円周の分割数です。
          let heightSegments = 32; //高さの分割数です。
          //let openEnded = false; //true:フタをしない,false:フタをする
          let openEnded = true; //true:フタをしない,false:フタをする

          const cylinder01 = new THREE.Mesh( 
              //円柱のジオメトリー(上面半径,下面半径,高さ,円周分割数)
              new THREE.CylinderGeometry(radiusTop, radiusBottom, objHeight, radiusSegments, heightSegments, openEnded),
              cylinderMaterial01
          );

          cylinder01.position.set(2, 0, 0); //(x,y,z)
          scene.add(cylinder01);

          tick();

          // 毎フレーム時に実行されるループイベントです
          function tick() {
            cylinder01.rotation.y += 0.01;
            cylinder01.rotation.x += 0.015;
            cylinder01.rotation.z += 0.02;
            renderer.render(scene, camera); // レンダリング
            requestAnimationFrame(tick);
          }
        }
      );
      // ↑ 筒 -----------------------------------------------------------

      //環境光源(アンビエントライト):すべてを均等に照らす、影のない、全体を明るくするライト
      const ambient = new THREE.AmbientLight(0xFFFFFF, 0.9);
      scene.add(ambient); //シーンにアンビエントライトを追加

      //平行光
      const light = new THREE.DirectionalLight(0xFFFFFF, 1);
      light.position.set(200,200,300);
      scene.add(light);
    }
  </script>
</head>
<body>
  <canvas id="myCanvas"></canvas>
</body>
</html>

1-3.ポイント

ポイントは2つ

  • CylinderGeometryのフタを外す
//let openEnded = false; //true:フタをしない,false:フタをする
let openEnded = true; //true:フタをしない,false:フタをする

//円柱のジオメトリー(上面半径,下面半径,高さ,円周分割数)
new THREE.CylinderGeometry(radiusTop, radiusBottom, objHeight, radiusSegments, heightSegments, openEnded)
  • 両面にテクスチャを適用する
const cylinderMaterial01 = new THREE.MeshBasicMaterial(
{
    map:texture,
    side: THREE.DoubleSide
}
// ↑ side について
// THREE.FrontSide :表面だけ描画
// THREE.BackSide  :裏面だけを描画
// THREE.DoubleSide:両面を描画

2.Geometryクラスで筒を作ってみた

2-1.参考

【three.js】Geometryクラスで正三角柱をつくる
https://qiita.com/baikichiz/items/4d8cf1a4f0f58d986152

2-2.各点の番号

配列 vertices に格納した順番で番号が振られる。(面を作成するときに使用)

2-3.スクリプト

Geometryクラスで筒.html
<!DOCTYPE html>

<html>
<head>
    <meta charset=utf-8>
    <title>My first three.js app</title>
    <style>
        body { margin: 0; }
        canvas { width: 100%; height: 100% }
    </style>
</head>
<body>

<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/105/three.min.js"></script>
<script>
// 三角柱をつくる。原点は底面正三角形の重心、柱の高さの半分の位置とする。
// length:
// height:
// depth:
function createTriangle(argWidth, argHeight, argDepth)
{

    var faceColor = 0x0000FF;
    var hW = argWidth / 2.0;
    var hH = argHeight/ 2.0;
    var hD = argDepth;

    var rate = 0.1
    var sW = argWidth / 2.0 - argWidth * rate;
    var sH = argHeight/ 2.0 - argHeight * rate;


    var vertices = [
        new THREE.Vector3(-hW,     0, -hH), // 0
        new THREE.Vector3( hW,     0, -hH), // 1
        new THREE.Vector3( hW,     0,  hH), // 2
        new THREE.Vector3(-hW,     0,  hH), // 3
        new THREE.Vector3(-hW,   -hD, -hH), // 4
        new THREE.Vector3( hW,   -hD, -hH), // 5
        new THREE.Vector3( hW,   -hD,  hH), // 6
        new THREE.Vector3(-hW,   -hD,  hH), // 7

        new THREE.Vector3(-sW,     0, -sH), // 8
        new THREE.Vector3( sW,     0, -sH), // 9
        new THREE.Vector3( sW,     0,  sH), // 10
        new THREE.Vector3(-sW,     0,  sH), // 11

        new THREE.Vector3(-sW,   -hD, -sH), // 11
        new THREE.Vector3( sW,   -hD, -sH), // 12
        new THREE.Vector3( sW,   -hD,  sH), // 13
        new THREE.Vector3(-sW,   -hD,  sH), // 14
    ];
    var faces = [
        // ↓ 上部 ↓ ----
        new THREE.Face3(0, 3, 8),
        new THREE.Face3(3, 11, 8),
        new THREE.Face3(3, 2, 11),
        new THREE.Face3(2, 10, 11),
        new THREE.Face3(2, 1, 10),
        new THREE.Face3(10, 1, 9),
        new THREE.Face3(1, 0, 9),
        new THREE.Face3(9, 0, 8),

        // ↑ 上部 ↑ ----
        new THREE.Face3(1, 2, 5),
        new THREE.Face3(6, 5, 2),
        new THREE.Face3(6, 2, 3),
        new THREE.Face3(7, 6, 3),
        new THREE.Face3(7, 3, 0),
        new THREE.Face3(4, 7, 0),
        new THREE.Face3(0, 1, 5),
        new THREE.Face3(0, 5, 4),
        // ↓ 下部 ↓ ----
        new THREE.Face3(4, 12, 7),
        new THREE.Face3(12,15, 7),
        new THREE.Face3(7, 15, 14),
        new THREE.Face3(7, 14, 6),
        new THREE.Face3(14, 5, 6),
        new THREE.Face3(14, 13, 5),
        new THREE.Face3(13, 4, 5),
        new THREE.Face3(13, 12, 4),
        // ↑ 下部 ↑ ----
    // 内側
        new THREE.Face3(13, 9, 8),
        new THREE.Face3(12, 13, 8),
        new THREE.Face3(8, 11, 12),
        new THREE.Face3(15, 12, 11),
        new THREE.Face3(11, 10, 14),
        new THREE.Face3(11, 14, 15),
        new THREE.Face3(13, 10, 9),
        new THREE.Face3(13, 14, 10),
        new THREE.Face3(8, 11, 12),
        new THREE.Face3(15, 12, 11),
    ];

    var geometry = new THREE.Geometry();
    var i = 0;
    for (i = 0; i < vertices.length; i++) {
        geometry.vertices.push(vertices[i]);
    }
    for (i = 0; i < faces.length; i++) {
        geometry.faces.push(faces[i]);
    }

    var material = new THREE.MeshBasicMaterial({ color: faceColor });

    // 三角柱のワイヤーフレームを描く
    var wireframeGeometry = new THREE.EdgesGeometry(geometry);
    var wireframeMaterial = new THREE.LineBasicMaterial({ color: 0xFFFFFF, linewidth: 4 });

    var triangleMesh = new THREE.Mesh(geometry, material);
    var wireframe = new THREE.LineSegments(wireframeGeometry, wireframeMaterial);

    triangleMesh.add(wireframe);
    return triangleMesh;
}

// x, y, z軸を赤、緑、青で描く
// length: 軸の長さ
function createAxes(length)
{
    var createOneAxis = function (color, vertex) {
        var material = new THREE.LineBasicMaterial({
            color: color
        });
        var vertices = [
            new THREE.Vector3(10, -10, -10),
            vertex
        ];
        var geometry = new THREE.Geometry();
        geometry.vertices = vertices;

        var line = new THREE.Line(geometry, material);
        return line;
    };

    return [
        createOneAxis(0x770000, new THREE.Vector3(length, 0, 0)),
        createOneAxis(0x007700, new THREE.Vector3(0, length, 0)),
        createOneAxis(0x000077, new THREE.Vector3(0, 0, length))
    ];
}


   window.onload = function() {

    var scene = new THREE.Scene();
    var camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
    var renderer = new THREE.WebGLRenderer();
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);

    var mesh = createTriangle(20, 20, 20);  scene.add(mesh);
    var axes = createAxes(100); axes.forEach(function (a) { scene.add(a);})
    //----------------------------//
    var hW = 10;
    var hH = 10;
    var hD = 20;

        var rate = 0.1
        var sW = hW - hW * 2 * rate;
        var sH = hH - hW * 2 * rate;

    //----------------------------//

    camera.position.z = 30;
    camera.position.y = 30;
    camera.position.x = 15;
    camera.lookAt(0, 0, 0);

    function animate() {
        mesh.rotation.x += 0.01;
        mesh.rotation.y += 0.01;
        requestAnimationFrame(animate);
        renderer.render(scene, camera);
    }

    /////-- ↓ マウスイベント ↓ --//////
    var mousedown = false;
    renderer.domElement.addEventListener('mousedown', function(e) {
            mousedown = true;
    }, false);

    renderer.domElement.addEventListener('mousemove', function(e) {
        if (!mousedown) return;
        mesh.rotation.x += 0.1;
        mesh.rotation.y += 0.1;
        render();
    }, false);

    renderer.domElement.addEventListener('mouseup', function(e) {
            mousedown = false;
    }, false);

    function render(){
            renderer.render(scene, camera);
    }
    /////-- ↑ マウスイベント ↑ --//////
    animate();
   }
</script>
</body>
</html>

おわりに

個人開発のWebARコンテンツ ウソ穴 に必要な部品作りとして調査しました。
Geometryクラスで筒を作るのは、面倒な上に筒っぽさの完成度が低い。

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

Select2の日本語化 「No results found」

スクリーンショット 2020-03-18 18.44.56.png

Select2の日本語化のCDNを読み込ませると良い。

<!doctype html>
<html lang="ja">
  <head>
    <!-- Select2本体 -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.2/js/select2.min.js"></script>
    <!-- Select2日本語化 -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.2/js/i18n/ja.js"></script>
  </head>
  <body>
  </body>
</html>

参考記事:select2 v4で日本語対応してtagsのバグを回避した方法(根治ではない

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

【JavaScript】ECMAScriptとかBabelとか

ECMAScript仕様

ECMAScriptとは、JavaScriptの標準規格です。
その仕様に改訂にあたって版(edition)が更新されていきます。
1997年に初版(ECMAScript First Edition)が公開され、1998年にはES2が1999年にはES3が公開されました。

ES4では、クラスやインターフェースなどを含めた新しい概念がいろいろ追加される予定でしたが、これは破棄されました。
そして、2009年になりようやくECMAScript Fifth Edition(ES5)として公開されました。strictモードやgetter,setterなどが追加されましたが、ES4で追加予定の多くの機能は実装されず、小規模な改善にとどまりました。

さらに時は流れ、2015年になり、ECMAScript Sixth Edition(ES6)が公開されました。これは、15年間で最初の大きな改訂となります。
let、const、クラス、モジュール、アロー関数、分割代入、などなど多くの新しい機能が追加されました。

そして、ES6は毎年新しいバージョンをリリースするという当初の構成に合わせて、ES2015に改名されます。
現在は、毎年ECMAScriptは改定され、ES2019が公開されています。

だれがどのようにESMAScriptの仕様を決めるか

ECMAScript仕様の開発と管理は、Ecma InternationalのTC39というタスクグループが担当しています。TC39のメンバーは、Mozilla、Google、Microsoft、Appleといった、Webブラウザを構築している企業で構成されています。

ECMAScriptの仕様に追加される機能は、ステージ0からステージ4までの5つのステージで審議されます。

  • ステージ0(ストローマン)
  • ステージ1(プロポーザル)
  • ステージ2(ドラフト)
  • ステージ3(キャンディート)
  • ステージ4(フィニッシュ)

JavaScriptの新しい機能を試してみたいと思ったときには、Babelなどのツールを使用してステージングを選択して利用することができます。

ステージ3以降は変更があったとしてもごくわずかであるため、使用するのは安全だと考えられますが、それよりも下のステージは将来機能が変更になったり、撤回される可能性があります。

Babelとは

JavaScriptの仕様の改訂について説明しました。現在は毎年仕様が改訂されていますが、その更新がすぐにブラウザに反映されるわけではありません。
だからといって、新しい機能をブラウザがサポートするのをまっているわけにはいきません。
そこで、新しい構文(ESNext)を、ほぼすべてのブラウザがサポートしている(ES5)に変換(トランスパイル)しようということになりました。
それをサポートするのが、Babelというツールというわけです。

ちなみに、BabelはかつてES6のコードをES5にトランスパイルしていたのでES6to5と呼ばれていました。その後JavaScriptの将来の機能を全てサポートするようになったことや、ES6の名前が正式にES2015に変更されたことを受けて、プロジェクト名をES6to5からBabelに変更することになちました。

Babelをセットアップする

プロジェクトの作成

Babelのインストールにはnpmが必要です。
Babelを使用する新しいディレクトリを作成して、npm initでプロジェクトを初期化します。

mkdir my-project
cd my-project
npm init -y

すると、ルート配下にpackage.jsonが作成されます。

package.json
{
  "name": "my-project",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

Babelコマンドラインインターフェースのインストール

次に、Babelのコマンドラインインターフェースをインストールします。
今回はBabel7をインストールします。

npm install @babel/core --save-dev
npm install @babel/cli --save-dev

プリセットのインストール

しかし、バージョン6以降のBabelでは、デフォルトでは何も変換されません。
変換を有効にするには、そのためのプリセットをインストールして、そのプリセットを使用することをBabelの構成で指定する必要があります。

Babekの構成には.babelrcというファイルを使用します。
これをプロジェクトのルート配下に配置します。

touch .babelrc

これを次のように編集します。

{
  "presets": ["@babel/preset-env"]
}

プリセットにはbabel-preset-envを指定しました。
babel-preset-envは、ターゲットのブラウザ、環境に合わせて自動でBabelプラグインを決定してくれるものです。
Babel7では、以前使用されていたbabel-preset-es2015などの年号プリセットは非推奨となっています。

次に、babel-preset-envをインストールします。

npm install @babel/preset-env --save-dev

npm scriptのセットアップ

最後に、Babelコマンドをnpm scriptとしてセットアップします。
npm scriptとは、package.jsonscriptsに書いてあるやつです。

package.json
"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },

npm scriptには、シェルコマンドを定義することができ、エイリアスのように使うことができます。
Babelでトランスパイルするコマンドを定義しておきましょう。

package.json
 "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "babel src -d dist"
  },

srcフォルダのファイルを、distフォルダに出力する、という感じです。
"test"の行末にセミコロンを追加することを忘れないようにしてください。

実行するときには、npm run buildとコマンドを打ちます。

トランスパイルの実行

これでトランスパイルする準備は完了しました。
プロジェクトにsrcフォルダを追加して、このフォルダにindex.jsファイルを追加します。

mkdir src
touch src/index.js

index.jsの中身で、ES2015の構文であるアロー関数、constを使用しています。

src/index.js
const plus = (num1, num2) => num1 + num2

console.log(plus(1, 1))

これをトランスパイルしてみます。

npm run build

distフォルダが作成され、その中にトランスパイルされたコードが含まれています。
確かに、ES5の構文に変換されています。

dist/index.js
"use strict";

var plus = function plus(num1, num2) {
  return num1 + num2;
};

console.log(plus(1, 1));

また、実行結果に違いはありません。

node src/index
2
node dist/index
2

しかし、Babelだけでは不十分で、モジュール化されたファイルをトランスパイルすることはできません。
モジュール化されたファイルを1つのファイルにバンドルする必要があります。
JavaScriptのモジュールをバンドルするツールとしてよく使われているのが、WebpackやBrowserifyです。

参考

入門JavaScriptプログラミング
ECMAScript
npm-script
Babelの手ほどき Babelとは
babel-preset-env

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

【SpecTest GUI - 2】MonacoEditor + Vue.js/Electron

SpecTest GUI ヘの道(2)

誰向け?

  • VSCode で使われている Monaco Editor に興味ある人
  • Monaco Editor で Markdown Editor を Electron ベースで 作りたい人
  • SpecTest を 応援してくれる

尚、今回の結果も以下にコミットしてあります。また、前回のには tag v0.1.0 をつけてあります。

はじめに

SpecTest は私が欲しいと思っていた BDD を実現するための汎用フレームワーク。

  • SpecTest そのものについては ここ を参照してください。
  • リポジトリは ここ です。

SpecTest GUI への道 と題して GUI 作っていきます。Kinx と両方並行して進めます。GUI 作りはサイド・プロジェクト。

前回の訂正

前回 の fontawesome の登録部分が足りてなかったので修正します。すみません。

main.js
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { library } from '@fortawesome/fontawesome-svg-core'
import { fas } from '@fortawesome/free-solid-svg-icons'
import { fab } from '@fortawesome/free-brands-svg-icons'
import { far } from '@fortawesome/free-regular-svg-icons'
library.add(fas, far, fab)
Vue.component('font-awesome-icon', FontAwesomeIcon)

FontAwesomeIcon の import と Vue.component('font-awesome-icon', FontAwesomeIcon) がなかった...

同期スクロール

基本、スクロールを検知したら相手側もスクロールさせる、ということを実装する。ただし、自動的にスクロールさせた結果も反対側で「スクロールした!」と反応して戻ってくるので何もしないと ループしてしまう。これを抑止しなければならない。

スクロール・ハンドラの追加

エディタ側は Monaco Editor の機能を使う。Monaco Editor には onDidScrollChange というインタフェースがあるので、そこにハンドラを登録する。

Editor.vue
  mounted () {
    var editor = this.$refs.editor.getEditor()
    editor.onDidScrollChange(this.handleScroll)
  },

ビューワ側は DOM オブジェクトなので、リスナに登録する。ここでは id 属性をつけておいて DOM オブジェクトを取得するようにしている。

Viewer.vue
  mounted () {
    document.getElementById("viewer").addEventListener('scroll', this.handleScroll);
  },
  beforeDestroy () {
    document.getElementById("viewer").removeEventListener('scroll', this.handleScroll);
  },

スクロール通知

スクロールの通知は親コンポーネントである MarkdownPane を介して行う。それぞれからイベントを受け取り、相手側に値を転送させる。

尚、今回は 簡易的な 位置合わせベースのスクロール連動です。というのも、レイアウトがエディタとビューワで変わるので精密に連動させるのは結構手間がかかる。今回は、全体に対する位置を互いに転送しあってその位置まで動く、というだけのものになる。これでも大体のケースにおいてまあまあうまく機能するし、そもそも世の中の メジャーな Markdown Editor もそんな動きしかしていない ので良いでしょう。

MarkdownPane.vue は以下のような感じ。@onScrollUpdatedViewer@onScrollUpdatedEditor でそれぞれ相手側に位置情報を転送します。誤差があったので、1 以下に丸めている。

MarkdonPane.vue
<template>
  <splitpanes horizontal :style="{ height: height, overflow: 'hidden' }" @resized="resizedPane($event)">
    <pane>
      <splitpanes :style="{ overflow: 'hidden' }" @resized="resizedPane($event)">   
        <pane class="pane-editor" ref="epane" size="55">
          <MarkdownEditor ref="editor" @onScrollUpdatedViewer="onScrollUpdatedViewer" />
        </pane>
        <pane class="pane-view" ref="vpane">
          <MarkdownViewer ref="viewer" @onScrollUpdatedEditor="onScrollUpdatedEditor" />
        </pane>
      </splitpanes>
    </pane>
  </splitpanes>
</template>

<script>
...
  methods () {
    ...
    onScrollUpdatedEditor (value) {
      this.$refs.editor.setScrollTop(value > 1 ? 1 : value);
    },
    onScrollUpdatedViewer (value) {
      this.$refs.viewer.setScrollTop(value > 1 ? 1 : value);
    },
  },
...
</script>

設定するインタフェースは setScrollTop メソッドをそれぞれ用意しておく。次のループ抑制の中で一緒に説明する。

ループ抑制

ループの抑制だが、単にフラグではうまくいかない。グリっとスクロールさせるといっぺんにいくつものスクロールイベントが発生するのでフラグの設定・解除漏れが発生する。ここでは相手からの通知があったら一定期間反対側からのスクロール・イベントはキャンセルするようにさせた。最後にイベントを受け取ってから 200 ミリに指定してある。

エディタ側は Monaco Editor の setScrollTop() メソッドを使う。this.clientHeight を引いているのは、scrollTop の値は画面の上部になるので、一画面分上になるからです。ビューワ側も同様。

Editor.vue
  data: () => {
    isScrollReceived: false,
    ...
  },
  methods: {
    ...
    setTimeout (clearOnly) {
      if (this.timeoutId) {
        clearTimeout(this.timeoutId)
        this.timeoutId = null
      }
      if (!clearOnly) {
        this.timeoutId = setTimeout(() => {
          this.isScrollReceived = false
          this.timeoutId = null
        }, 200)
      }
    },
    setScrollTop (v) {
      this.isScrollReceived = true
      this.setTimeout(false)
      var el = this.$refs.editor;
      var editor = el.getEditor()
      var topEnd = editor.getScrollHeight() - this.clientHeight
      this.$nextTick(() => {
        editor.setScrollTop(topEnd * v);
      })
    },
    handleScroll () {
      if (this.isScrollReceived) {
        return
      }
      var editor = this.$refs.editor.getEditor()
      var scrollTop = editor.getScrollTop();
      var topEnd = editor.getScrollHeight() - this.clientHeight
      if (topEnd > 0) {
        this.$nextTick(() => {
          this.$emit('onScrollUpdatedViewer', scrollTop / topEnd)
        })
      }
    },
  },

ビューワ側は DOM のプロパティにセットするだけ。

Viewer.vue
  data: () => {
    isScrollReceived: false,
    ...
  },
  methods: {
    ...
    setTimeout (clearOnly) {
      if (this.timeoutId) {
        clearTimeout(this.timeoutId)
        this.timeoutId = null
      }
      if (!clearOnly) {
        this.timeoutId = setTimeout(() => {
          this.isScrollReceived = false
          this.timeoutId = null
        }, 200)
      }
    },
    setScrollTop (v) {
      this.isScrollReceived = true
      this.setTimeout(false)
      var el = this.$refs.viewer;
      var topEnd = el.scrollHeight - el.clientHeight
      this.$nextTick(() => {
        el.scrollTop = topEnd * v;
      })
    },
    handleScroll (e) {
      if (this.isScrollReceived) {
        return
      }
      var el = e.target
      if (el && el.clientHeight && el.scrollHeight) {
        var topEnd = el.scrollHeight - el.clientHeight
        if (topEnd > 0) {
          this.$nextTick(() => {
            this.$emit('onScrollUpdatedEditor', el.scrollTop / topEnd)
          })
        }
      }
    },
  },

やってみる。

SyncScroll

静止画ではわからないとは思うが、無事、スクロールが同期した。

一番上に戻る

長い文章になると一番上に戻りたくなるよね。付けましょう。よくあるフローティング・ボタンを右下につけて、一番上まで戻る機能を。

フローティング・ボタンは全体で一番右下なので、App.vue に追加。以下のように一番下に追加しておく。

App.vue
<template>
  <v-app>
    <v-app-bar app ref="appbar" height="56">
      <v-app-bar-nav-icon></v-app-bar-nav-icon>
      <v-toolbar-title>SpecTest GUI</v-toolbar-title>
    </v-app-bar>

    <v-content>
      <MarkdownPane ref="pane" />
    </v-content>
    <v-btn color="red" dark fixed right bottom fab @click="gotoTop"><font-awesome-icon icon="chevron-up" /></v-btn>
  </v-app>
</template>

MarkdownPane に ref を付けておき、ボタンが押されたら gotoTop を通知。

App.vue
  methods: {
    gotoTop () {
      this.$refs.pane.gotoTop()
    },
  },

MarkdownPane.vue では、gotoTop を受け取ったら ビューワのほうにだけ 通知。ビューワのほうが簡単なので。el.scrollTo がいい具合にスムーズにアニメーションしてくれる。また、ビューワ側でスクロールするとさっきの同期処理が働いて自動的にエディタ側もスクロールしてくれる!素晴らしい。

MarkdownPane.vue の実装は methods にこれを追加するだけ。とりあえず、null を渡すのをサインにした。

MarkdownPane.vue
    gotoTop () {
      this.$refs.viewer.setScrollTop(null);
    },

ビューワ側の実装はこんな感じ。setScrollTop の先頭に null の場合を追加。

Vuewer.vue
    setScrollTop (v) {
      if (v == null) {
        this.$refs.viewer.scrollTo({
          top: 0,
          behavior: "smooth"
        })
        return
      }
      ...

実行。

GotoTop1

さあ、ボタンを押してみよう。

GotoTop2

(静止画ではわかりづらいが)いい感じにスムーズ・スクロールした! ...そして違和感なくスムーズ・スクロールのまま同期するのも気分がいい。ビューン

編集機能

さて、ついでに Monaco Editor の編集機能を充実させます。基本キーボードで操作するのが楽なのだが、念のためツールバーもつけておきます。でもテキストエディタでツールバーって実は使いづらいよね。

ショートカットキー

まずはショートカットキーから。Monaco Editor の場合、アンドゥ機能を有効にするためには実質メソッドはこれしかない。

editor.getModel().pushEditOperations(...)

これで全てのことを実現する。replace とか insert とか気の利いた名前のメソッドは全くない。今回は、選択した文字列を特定の文字列で括る(例えば aaa**aaa** とか)とヘッダを付ける機能を用意する。ここで @editorWillMount イベントで受け取った monaco オブジェクトが必要になる。

文字列ラッピング・コマンド

まずは、汎用的なコマンド関数の生成関数(ジェネレータ)を作っておく。大体やること一緒なので。
ラッピング・コマンドの生成関数は次の通り。コマンド実行のクロージャ―を返す。

Editor.vue
function generateWrapperCommand(monaco, editor, startText, endText) {
  var len = startText.length + endText.length
  return () => {
    var sels = editor.getSelections()
    if (sels == null) {
      return
    }
    var ranges = []
    sels.forEach(selection => {
      ranges.push(new monaco.Selection(selection.startLineNumber, selection.startColumn, selection.endLineNumber, selection.endColumn+len))
      editor.getModel().pushEditOperations([], [
          {
              range: {
                  startLineNumber: selection.startLineNumber,
                  startColumn: selection.startColumn,
                  endLineNumber: selection.startLineNumber,
                  endColumn: selection.startColumn
              },
              text: startText
          },
          {
              range: {
                  startLineNumber: selection.endLineNumber,
                  startColumn: selection.endColumn,
                  endLineNumber: selection.endLineNumber,
                  endColumn: selection.endColumn
              },
              text: endText
          }
      ])
    })
    editor.setSelections(ranges)
    return null
  }
}

pushEditOperations の第二引数にオペレーションを登録していくが、start と end が一緒ならその位置に挿入、異なっているならその範囲を置換、と思えばよい。最後に挿入後の新しい範囲を指定するようにしておく。

これを使ったショートカット・コマンドを登録する汎用関数を作っておく。ちなみに、actionCommand[label] に登録しているのは、後でツールバーからコマンド名で実行できるようにするため。

Editor.vue
var actionCommand = {}, cmdid = 0;

function addWrapperCommand(monaco, editor, context, label, keybindings, startText, endText) {
  actionCommand[label] = generateWrapperCommand(monaco, editor, startText, endText)
  editor.addAction({
    id: 'markdwon-'+(cmdid++),
    label: label,
    keybindings: keybindings,
    contextMenuGroupId: context && 'navigation',
    contextMenuOrder: context && 1.5,
    run: actionCommand[label]
  })
}

追加は editoraddAction を使う。context... を指定しておくことで、label に指定した名前でコンテキストメニューにも追加されて一石二鳥だ。では、これでいくつかコマンドを登録してみよう。キーボードバインディングがこれで使いやすいかは別として、登録の仕方はわかると思う。

Editor.vue
function setupShortcutKeys(monaco, editor) {
  addWrapperCommand(monaco, editor, true , "Bold",           [ monaco.KeyMod.CtrlCmd | monaco.KeyCode.KEY_B ],                        "**", "**")
  addWrapperCommand(monaco, editor, true , "Italic",         [ monaco.KeyMod.CtrlCmd | monaco.KeyCode.KEY_I ],                        "*",  "*")
  addWrapperCommand(monaco, editor, true , "Underline",      [ monaco.KeyMod.CtrlCmd | monaco.KeyCode.KEY_U ],                        "<u>",  "</u>")
  addWrapperCommand(monaco, editor, true , "Strikethrough",  [ monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KEY_S ],  "~~", "~~")
  addWrapperCommand(monaco, editor, true , "Code",           [ monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KEY_M ],  "`",  "`")
  addWrapperCommand(monaco, editor, false, "Link-1",         [ monaco.KeyMod.CtrlCmd | monaco.KeyCode.KEY_L ],                        "[",  "]()")
  addWrapperCommand(monaco, editor, false, "Link-2",         [ monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KEY_L ],  "[](",  ")")
}

この setupShortcutKeysmounted のときに呼び出す。一応、this.monacomouted の前に設定されることになっている(実際はここでチェックする必要はない)。

Editor.vue
  mounted () {
    var editor = this.$refs.editor.getEditor()
    editor.onDidScrollChange(this.handleScroll)
    if (this.monaco) {
      setupShortcutKeys(this.monaco, editor)
    }
  },

では、ヘッダを追加するショートカット・コマンドも追加してみる。今度は行頭に ## 等をつけるというもの。元々ある場合は差し替える動作が自然だろう。同じように登録用関数を定義する。

Editor.vue
function generateHeaderCommand(monaco, editor, startText) {
  return () => {
    var sels = editor.getSelections()
    if (sels == null) {
      sels = [editor.getSelection()]
    }
    sels.forEach(selection => {      
      var m = editor.getModel().findNextMatch("^(#+ )", { lineNumber: selection.startLineNumber, column: 1 }, true, false, null, true)
      if (m != null) {
        if (m.range.startLineNumber == selection.startLineNumber && m.range.startColumn == 1) {
          if (m.matches[1] == startText) {
            return
          }
          editor.getModel().pushEditOperations([], [
              {
                  range: {
                      startLineNumber: selection.startLineNumber,
                      startColumn: 1,
                      endLineNumber: selection.startLineNumber,
                      endColumn: m.matches[1].length + 1
                  },
                  text: ''
              }
          ])
        }
      }
      editor.getModel().pushEditOperations([], [
          {
              range: {
                  startLineNumber: selection.startLineNumber,
                  startColumn: 1,
                  endLineNumber: selection.startLineNumber,
                  endColumn: 1
              },
              text: startText
          }
      ])
    })
    return null
  }
}

function addHeaderCommand(monaco, editor, context, label, keybindings, startText) {
  actionCommand[label] = generateHeaderCommand(monaco, editor, startText)
  editor.addAction({
    id: 'markdwon-'+(cmdid++),
    label: label,
    keybindings: keybindings,
    contextMenuGroupId: context && 'navigation',
    contextMenuOrder: context && 1.5,
    run: actionCommand[label]
  })
}

同じ文字列があるかどうかは editor.getModel().findNextMatch(...) を使う。正規表現も使えるので便利。見つからなかった場合、null が返る。ただし、スタート位置を指定できるが最後まで行くと先頭に戻る動作をする模様。そこで、見つかった場合は同じ行の先頭かどうかを確認する。同じ行の先頭だった場合は一旦削除する。今と同じなら何もしない。

これをショートカット・コマンドとして登録しよう。

Editor.vue
function setupShortcutKeys(monaco, editor) {
  ...
  addHeaderCommand (monaco, editor, false, "Header 1", [ monaco.KeyMod.CtrlCmd | monaco.KeyMod.Alt   | monaco.KeyCode.KEY_1 ],  "# ")
  addHeaderCommand (monaco, editor, false, "Header 2", [ monaco.KeyMod.CtrlCmd | monaco.KeyMod.Alt   | monaco.KeyCode.KEY_2 ],  "## ")
  addHeaderCommand (monaco, editor, false, "Header 3", [ monaco.KeyMod.CtrlCmd | monaco.KeyMod.Alt   | monaco.KeyCode.KEY_3 ],  "### ")
  addHeaderCommand (monaco, editor, false, "Header 4", [ monaco.KeyMod.CtrlCmd | monaco.KeyMod.Alt   | monaco.KeyCode.KEY_4 ],  "#### ")
  addHeaderCommand (monaco, editor, false, "Header 5", [ monaco.KeyMod.CtrlCmd | monaco.KeyMod.Alt   | monaco.KeyCode.KEY_5 ],  "##### ")
  addHeaderCommand (monaco, editor, false, "Header 6", [ monaco.KeyMod.CtrlCmd | monaco.KeyMod.Alt   | monaco.KeyCode.KEY_6 ],  "###### ")
}

ツールアイコン

アイコン

さて、せっかくなのでツールバーを作ってみる。イマイチ使いづらい気もするが、まぁ使わなければ後で消す。まずはアイコンから。長くないので全部載せる。以下のような修正。

  • アイコン用の領域を作るが、高さを計算できるようにするため、高さを補正するコードを追加。
  • アイコンが押されたときに、エディタに指示。エディタは action というメソッドを用意しておく。
MarkdownPane.vue
<template>
  <splitpanes horizontal :style="{ height: height, overflow: 'hidden' }" @resized="resizedPane($event)">
    <pane :size="toobarSize">
      <div style="margin-left: 8px">
        <v-btn :style="{ marginTop: btnMargin }" class="btn-item" color="primary" fab x-small @click="click('Bold')"><font-awesome-icon icon="bold" /></v-btn>
        <v-btn :style="{ marginTop: btnMargin }" class="btn-item" color="primary" fab x-small @click="click('Italic')"><font-awesome-icon icon="italic" /></v-btn>
        <v-btn :style="{ marginTop: btnMargin }" class="btn-item" color="primary" fab x-small @click="click('Underline')"><font-awesome-icon icon="underline" /></v-btn>
        <v-btn :style="{ marginTop: btnMargin }" class="btn-item" color="primary" fab x-small @click="click('Strikethrough')"><font-awesome-icon icon="strikethrough" /></v-btn>
        <v-btn :style="{ marginTop: btnMargin }" class="btn-item" color="primary" fab x-small @click="click('Code')"><font-awesome-icon icon="code" /></v-btn>
      </div>
    </pane>
    <pane :size="editorSize">
      <splitpanes :style="{ overflow: 'hidden' }" @resized="resizedPane($event)">   
        <pane class="pane-editor" ref="epane" size="55">
          <MarkdownEditor ref="editor" @onScrollUpdatedViewer="onScrollUpdatedViewer" />
        </pane>
        <pane class="pane-view" ref="vpane">
          <MarkdownViewer ref="viewer" @onScrollUpdatedEditor="onScrollUpdatedEditor" />
        </pane>
      </splitpanes>
    </pane>
  </splitpanes>
</template>

<script>
import MarkdownEditor from './markdown/Editor'
import MarkdownViewer from './markdown/Viewer'
import { Splitpanes, Pane } from 'splitpanes'
import 'splitpanes/dist/splitpanes.css'

export default {
  name: 'MarkdownPane',

  components: {
    MarkdownEditor, MarkdownViewer, Splitpanes, Pane,
  },

  data: () => ({
    toolbarPx: 60,
  }),

  methods: {
    click (name) {
      this.$refs.editor.action(name);
    },
    resizedPane () {
      this.$nextTick(() => {
        this.$refs.editor.resize(this.$refs.epane.$el, this.$store.state.windowSize.height - this.toolbarPx)
        this.$refs.viewer.resize(this.$refs.vpane.$el, this.$store.state.windowSize.height - this.toolbarPx)
      })
    },
    gotoTop () {
      this.$refs.viewer.setScrollTop(null);
    },
    onScrollUpdatedEditor (value) {
      this.$refs.editor.setScrollTop(value > 1 ? 1 : value);
    },
    onScrollUpdatedViewer (value) {
      this.$refs.viewer.setScrollTop(value > 1 ? 1 : value);
    },
  },

  computed: {
    height () {
      return (this.$store.state.windowSize.height - 1) + "px"
    },
    btnMargin () {
      var top = ((this.toolbarPx - 4 - 32) / 2 + 4) + "px"
      return top
    },
    toobarSize () {
      return this.toolbarPx * 100 / this.$store.state.windowSize.height
    },
    editorSize () {
      return (this.$store.state.windowSize.height - this.toolbarPx) * 100 / this.$store.state.windowSize.height
    },
  }
};
</script>

<style scoped>
.btn-item {
  margin-left: 2px;
  margin-right: 2px;
}
</style>

エディタの action は以下の通り。

Editor.vue
    action (name) {
      if (actionCommand[name] != null) {
        actionCommand[name]()
      }
    },

これだけ。さっき actionCommand に関数登録しておいたおかげでそのまま使える。クロージャ―になっているので、monaco とか editor とかも内部でちゃんと使えて問題ない。

画面上はこんな感じになる。うん、マテリアル。

Toolbar

コマンド追加は同じ方法で可能なので、今後必要に応じて追加する。

おわりに

さて、Markdown Editor もいい感じにできた。というか、これベースにオリジナルで使いやすい Markdown Editor 作るのもアリじゃないか?というくらい個人的には出来がいいな。

次からは本当の意味で SpecTest に対応していこう。ただ、諸事情あってちょっとペースが落ちるかも。。。
ここまでの結果は、以下にコミットしてあります。v0.2.2 としてタグも打ってあります。

SpecTest そのものに関しては以下を参照してください。

ではまた次回。

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

iGarage 体験会カリキュラム

JavaScriptでおみくじを作ってみよう

今日のゴール

ボタンをクリックするとランダムで運勢が表示されるおみくじを作ってみましょう。

スクリーンショット 2020-03-30 0.51.59.png

     ↓クリック↓

スクリーンショット 2020-03-30 0.51.53.png

使用するファイル

MyOmikujiフォルダ内にある、index.html,styles.css,main.jsを編集していきます。

  • index.htmlでは全体の見た目
  • styles.cssではスタイル
  • main.jsではアニメーション

を主に作ります。

見た目の実装

まずはindex.htmlで全体の見た目を作っていきます。

テキストエディタでindex.htmlを開いて、以下を貼り付けましょう。

index.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>おみくじ</title>
  <link rel="stylesheet" href="css/styles.css">
</head>
<body>
  <script src="js/main.js"></script>
</body>
</html>

これはHTMLを書く際の決まりのようなものです。

次にボタンとなる要素を作ります。
<body>の下の行に、

index.html
  <div id="btn">?</div>

を入れましょう。
最初ボタンに表示される?を入れておきました。
また、id="btn"と入れておくと、後でJavaScriptから扱いやすくなります。

ここまで出来ているか、index.htmlをブラウザで開いて確認してみましょう。
?が表示されていればOKです。

ボタンのスタイルを作ろう

次に、styles.cssを編集してボタンのスタイルを作っていきましょう。

先程、id="btn"として?を表示した部分をボタンの形にしましょう。
styles.cssを開いて、

styles.css
#btn {
  width: 200px;  /* 幅: 200px */
  height: 200px;  /* 高さ: 200px */
  background: #ef454a;  /* 背景色: #ef454a */
  border-radius: 50%;  /* ボタンを円形に */
  margin: 30px auto;  /* ボタンの位置を上から30px,左右の余白を均等に */
}

一度、ブラウザを更新して確認してみましょう。

スクリーンショット 2020-03-30 1.35.20.png

?の位置がずれてしまっているので、直してみましょう。

margin: 30px auto;の下の行を追加します。

styles.css
#btn {
  width: 200px;  /* 幅: 200px */
  height: 200px;  /* 高さ: 200px */
  background: #ef454a;  /* 背景色: #ef454a */
  border-radius: 50%;  /* ボタンを円形に */
  margin: 30px auto;  /* ボタンの位置を上から30px,左右の余白を均等に */
  /* ここから */
  text-align: center;  /* 左右中央揃え */
  line-height: 200px;  /* 高さ調整 */
  color: #fff;  /* 文字の色: 白 */
  font-weight: bold;  /* 太字 */
  font-size: 42px;  /* 文字の大きさ: 42px */
  /* ここまで */
}

ブラウザを更新して確認してみましょう。

スクリーンショット 2020-03-30 2.36.58.png

ボタンをクリックした操作を作ろう

ボタンをクリックできるようにして、クリックした時の処理を書いていきましょう。

main.jsを開いて、

main.js
'use strict';

を入れましょう。これによって厳密なエラーチェックをするようにします。
その下の行に、

main.js
{
  const btn = document.getElementById('btn');

  btn.addEventListener('click', () => {
    btn.textContent = 'hit!';
  });
}

を入れましょう。

ブラウザを更新して変化を見てみましょう。
ボタンをクリックするとhit!と表示されるようになりました。

スクリーンショット 2020-03-30 2.39.43.png

ここで、ボタンがクリックできることがわかるようにスタイルを追加し、さらに、ボタンの形を整えていきましょう。

styles.cssを開いて、
font-size: 42px;の下の行を追加します。

styles.css
#btn {
  width: 200px;  /* 幅: 200px */
  height: 200px;  /* 高さ: 200px */
  background: #ef454a;  /* 背景色: #ef454a */
  border-radius: 50%;  /* ボタンを円形に */
  margin: 30px auto;  /* ボタンの位置を上から30px,左右の余白を均等に */
  text-align: center;  /* 左右中央揃え */
  line-height: 200px;  /* 高さ調整 */
  color: #fff;  /* 文字の色: 白 */
  font-weight: bold;  /* 太字 */
  font-size: 42px;  /* 文字の大きさ: 42px */
  /* ここから */
  cursor: pointer;  /* カーソルをポインターに */
  box-shadow: 0 10px 0 #d1483e;  /* ボタンの下に影をつける */
  user-select: none;  /* 中のテキストを選択不可に */
  /* ここまで */
}

一番下の行に、

styles.css
#btn:hover {
  opacity: 0.9;  /* ホバーした時に色を薄く */
}

/* クリックするとボタンが押し込まれたように */
#btn:active {
  box-shadow: 0 5px 0 #d1483e;
  margin-top: 35px;
}

を追加します。

ブラウザを更新して確認してみましょう。

乱数を表示してみよう

最終的にはランダムで運勢が表示されるようにします。
そのために、乱数を使ってランダムな数値を作ってみましょう。

JavaScriptでは、Math.random()という命令を使うことで0以上1未満のランダムな数値を生成出来ます。

main.js'hit!'の部分をMath.random()に変えます。

main.js
{
  const btn = document.getElementById('btn');

  btn.addEventListener('click', () => {
    btn.textContent = Math.random();
  });
}

このようになります。

ブラウザを更新して確認してみましょう。
0以上1未満のランダムな値が生成されるかと思います。

スクリーンショット 2020-03-30 3.03.02.png

条件分岐で運勢を表示しよう

条件分岐にはif文を使います。

まずは、先程の

main.js
{
  const btn = document.getElementById('btn');

  btn.addEventListener('click', () => {
    btn.textContent = Math.random();
  });
}

を消して、

main.js
{
  const btn = document.getElementById('btn');

  btn.addEventListener('click', () => {
    const n = Math.random();
  });
}

とします。
これでnをランダムな数値としています。

このnMath.random()によって、0以上1未満の乱数となるので、
例えば、大吉を22%の確率で表示したい場合は、

main.js
    if (n < 0.22) {
      btn.textContent = '大吉'
    }

をこの行:

main.js
    const n = Math.random();

の下の行に入れてあげれば良いでしょう。

また、大吉以外の場合も書いてあげましょう。

main.js
    if (n < 0.22) {
      btn.textContent = '大吉'
    } else if (条件2) {
      btn.textContent = '中吉'
    } else if (条件3) {
      btn.textContent = '小吉'
    } else {
      btn.textContent = ''
    }

といったように書いてあげます。
22%の確率で大吉、8%の確率で中吉、13%の確率で小吉、それ以外の場合はと表示させるとしたら、
この条件2条件3をどうすれば良いか考えてみましょう。

書けたら、コーチに見てもらいましょう。

最後に、他の運勢を追加したり確率を変えたりして、オリジナルのおみくじを完成させましょう。


体験会カリキュラムはこれで以上になります。

Web開発では、このような全体の見た目、スタイル、アニメーションなどを取り入れながら作っていきます。
体験会カリキュラムを通して、自分オリジナルのWebサイトのアイデアがふくらんだのではないでしょうか。

また、見た目だけでなく、サイトそのものの構造も自分で作って行くことになります。
詳しい内容はコーチに聞いてみてください。

iGarageでこれらをさらに深く学び、自分だけのWebサイトを作っていきましょう。

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

そのjs、cssで代用できませんか?(件数通知バッジ編)

webアプリケーションエンジニアからフロントエンドエンジニアになって約一年やってきた中で、
どうしたらjsを書く量を減らせるか?みたいな状況を解決した時のtips

why 件数通知バッジ

皆さん通知バッジ作ってますか?僕は作りました。
脳筋コードで書いたら意外とめんどくさいんですよねあいつ。

  • 数字が変わるとなると、疑似要素でやるのがめんどくさいので<span>とかで作りがち
  • 0件になったら消さなきゃいけない

とかやってるとjsがとても汚くなって悲しくなったので、いい感じにcss交えて書くといい感じになったよっていうお話です。

コード

See the Pen 通知バッジ by さむとる (@sumtrue) on CodePen.

jsはdata-numを++/--するだけで済んでるので、とても綺麗で読みやすいコードになりました

tips

attr()

https://developer.mozilla.org/ja/docs/Web/CSS/attr
htmlに付与された属性を取得できる関数。
今回はこれを使うことでjsとhtmlの繋ぎこみをしている。

まだ使ったことはないが型指定とか代替値も指定できる便利屋さん。
要素によって少しだけ変わることがある、みたいなのを実現もできる。

属性セレクター

https://developer.mozilla.org/ja/docs/Web/CSS/Attribute_selectors
htmlに付与された属性でcssをあてることのできるセレクター。
今回は0件のときにバッジを非表示にする際に使用。

その他オススメの使い方としてはこれ。


img:not([alt])でaltが指定されていない画像を見つけられるというやつ

この記事を書いた理由的な

「ぶっちゃけjs書けばなんでもできるじゃん」論者だったんですが、CSS組み合わせることでコードの可読性がめっちゃあがります。
「普段あまりcssはほとんど書かないけどjsは時々書く」「jsは基本書かないけどcssは結構書くよ」みたいな人たちに、
それぞれで何ができるかを知ってもらい、世の中のコードが綺麗になったらいいなと思った次第。

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

【日常】みよちゃんがゆっこに教えるJSタイマー

(とっさの思いつきで)

前置き

・触発記事はミルクボーイから。
https://qiita.com/cheez_RPA/items/5f2dbc768ddb34f520ff
・日常は中学の時に見ていた。
・ゆっこは、JS初学者

本編

ゆ:みよちゃん、みよちゃん
み:どうしたの、ゆっこ

ゆ:私ね、今日実は宿題をしようと思うの
み:急にどうしたの?!ゆっこが勉強するなんて地球に小惑星が衝突するようなものじゃない

ゆ:実は、お母さんに「アンタ、学校から連絡あったよ。もし次宿題やらなかったら、朝夕飯抜きだからね!」と言われてね。私の衣食住のうちの食が3分の2減りそうな危機なんだよ
み:まぁ、そんな所だろうとは思ったわ。それで?私に何をお願いしに来たの?

ゆ:さすが、みよちゃん!話が早いね。実は、JavaScriptで簡単なタイマーを作りたいの。そうすれば、その宿題に何分掛かったか分かるじゃない。どの教科の宿題がどれだけ時間が掛かるかが分かれば・・・逆算していつまで遊んで、いつから宿題すれば間に合うか分かるじゃない!私って天才
み:(あくまで遊ぶことが優先になっているな、コイツ。)分かったわ。それで?具体的にどんなものを作りたいの?

ゆ:んー。まずは簡単に時間が分かるタイマーがあればなぁ、なんて。実行した時からどれだけ時間が掛かっているかが一目瞭然になればイケてるじゃない!

10秒経過しました。
・・・
1分経過しました。
・・・
30秒経過しました。
・・・
2分経過しました。
・・・
40秒経過しました。

みたいな!
そしたら、(お、今は2分40秒か・・!)ってなるじゃん!私、こー見えて数字にはうるさいから、10秒ごとに表示されると良いかなぁ、なんて。
み:まぁ、分かったわ。それで?今どこまで進んでんの?

ゆ:ん?何が?
み:まさか、アンタ。何も進んでないのに私に相談したっていうの・・・

み:まずは、自分で出来るところまでやってから、こーーーーい!!!!!

1日後

ゆ:みおちゃん、みおちゃん!出来たよ!出来た!
み:やるじゃない!ゆっこ!早速見せてよ!

ゆ:はい、どうぞ!ちなみにだけど環境は、VSCodeってやつで書いたよー。

app.js
//ゆっこの一撃目
let count = 0;

const countUp = function() {
  count++;
}

const id = setInterval(function() {
  countUp();
  if(count % 5 == 0){
    console.log(count + '秒経過しました');
  } else if(count % 10 == 0){
    console.log(count / 10 + '分経過しました');
  }
}, 1000);

ゆっこの浅はかさ

ゆ:どうよ!試しに、30秒ぐらい試してみたけど、問題なく、動いてる!
スクリーンショット 2020-03-29 21.10.19.png

み:手に負えねぇえええ!ゆっこ!setIntervalを使ったのは凄く良いけど、中身の機能がまだ不完全よ!確かに

if(count % 5 == 0){
  console.log(count + '秒経過しました');
}

の部分は、正常に動くけど、後ろの

else if(count % 10 == 0){
  console.log(count / 10 + '分経過しました');
}

が動いていないわ。何事も早く終わらせたい性格が裏目に出たわね。1分後の結果は、ゆっこ、こうなるよ!
スクリーンショット 2020-03-29 21.14.30.png

ゆ:おんぎゃああああ嗚呼あああああああ!!!!!!!!なんでなの!!
み:if文はね。先に実行出来るものがあった場合、そっちを実行して、その後ろの文は飛ばしてしまうのよ!だから、ゆっこが書いたelse if文は飛ばされてしまうの!

ゆ:どどドドドドドドドドどうすれば良いの! はっ?! こうすれば良いのね!

app.js
// ゆっこの二撃目
let count = 0;

const countUp = function() {
  count++;
}

const id = setInterval(function() {
  countUp();
  if(count % 60 == 0){
    console.log(count / 60 + '分経過しました');
  } else if (count % 10 == 0){
    console.log(count + '秒経過しました');
  }
}, 1000);

み:落ち着くのよ!ゆっこ!ゆっこの浅はかさしか出ていないコードだわ!確かに、順序を入れ替えたのは良いけれど、1分以降がこうなってしまうわ。
スクリーンショット 2020-03-29 21.29.42.png

ゆ:おんぎゃああああ嗚呼あああああああ!!!!!!!!なんでなの!!これじゃ私はいつまで経ってもタイマーを作れないじゃないの・・・
み:落ち着いて!ゆっこ!例えば、新しく関数を作って、そっちで分単位の出力を任せれば良いじゃない!

ゆ:確かに・・・。みおちゃん天才!おっしゃああああああああああ!任せな、みおちゃん!
そうなると、コールバック関数とかイケてるヤツ使っちゃって、こんな感じでイケんじゃない?!

app.js
// ゆっこの三撃目
let count = 0;
let countUpTime = 0;

const countUp = function() {
  count++;
}

const countTime = function() {
  countUpTime++;
}
const id = setInterval(function() {
  countUp();
  if(count % 60 == 0){
    console.log(count / 60 + '分経過しました');
    myCallback();
  } else if (count % 10 == 0){
    console.log(count + '秒経過しました');
  }
}, 1000);

function myCallback(){
  countTime();
  console.log(countUpTime + '分経過しました')
  count = 0;
}

み:確かに、これならイケそうだわ。myCallback()の中身は、countTimeの実行から始まる・・・。countTimeはもともと0でそこから+1されるから、

console.log(countUpTime + '分経過しました')

の部分では、1分、2分・・・と出力される・・。それに count = 0; にすることで、また10秒から表示される・・・。やるわね、ゆっこ!!
ゆ:おうよ、みおちゃん!!これで問題なく・・・
スクリーンショット 2020-03-29 21.40.11.png

ゆ:ぬおおおおおお?!1分経過しました、が二回も出ているだトォ?!
み:ゆっこ。これは由々しき事態よ。麻衣ちゃんが見たら「ぷっ。ゆっこ、JSを書くことも出来ないのね」って笑われちゃうよ!

ゆ:ぬぬぬ。コードを改善していく必要があるな・・・!
ゆ:ならば・・・。これで、どうだぁああああああ!!!

app.js
// ゆっこの最後の一撃
let count = 0;
let countUpTime = 0;

const countUp = function() {
  count++;
}

const countTime = function() {
  countUpTime++;
}

const id = setInterval(function() {
  countUp();
  if(count % 60 == 0){
    myCallback();
    count = 0;
  } else if (count % 10 == 0){
    console.log(count + '秒経過しました');
  }
}, 1000);

function myCallback(){
  countTime();
  console.log(countUpTime + '分経過しました')
}

スクリーンショット 2020-03-29 21.49.42.png

ゆ:おっしゃぁああああああ!!!!!
み:やるじゃない、ゆっこ!!これで麻衣ちゃんにもけなされることはないはずよ!定数のidは、秒で動いていて、myCallbackは、分単位で動いているわね!それに、myCallback()の後に、count = 0;にすることで、1分毎に秒数を0にしているじゃない!

み:まだまだコードの改善はあるけれど、今のゆっこにしては上出来じゃない・・?
ゆ:みよちゃん・・。ありがとお!

み:ただ、もっとコードの改善は出来るはずよ。constとletについても詳しく教えたいし。けど、最初のトライ&エラーの繰り返しは、ゆっこにしては凄く良かったわ。この調子で、どんどんJSを強化していくわよ!
ゆ:はい、師匠!これからもよろしくお願いしますぜ!

最後に

み:(ゆっこが自分で考えて、自分の欲しい機能を作るなんて・・・)
み:(携帯のタイマー機能じゃダメなの?なんて聞かなくて良かったみたいね)

所感

・学生用の歯学本を作るから手伝ってと言われて、それぞれのReadingに何分掛かるか計る為に作った。
・しかし、日常をBGMにしたせいで作業が進まなかったし、このQiitaを書きたくなりウズウズしながら取り組んでいた。
・締め切り1時間前に何とか出来たが、日常をBGMにすることは辞めようと思う。
・ターミナルで Ctrl + cで止めて、↑ + Enterで即実行できるので、割と使いやすかった。

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