20210106のJavaScriptに関する記事は25件です。

【Vue(Vuetify)】1月は日本全国酒飲み音頭ジェネレーターを作ったので酒が飲めるぞ

この記事は、リンク情報システムが主催するイベント「2021新春アドベントカレンダー Tech Connect」のリレー記事です。
2021新春アドベントカレンダー Tech Connect」は engineer.hanzomon のグループメンバによってリレーされます。
(リンク情報システムのFaceBookはこちらから)

3日目となります本日は「【Vue(Vuetify)】1月は日本全国酒飲み音頭ジェネレーターを作ったので酒が飲めるぞ」という記事を@usankusaiが投稿いたします。

成果物

こんなの作ってみました。
https://sakenomi-5733e.web.app/
2021-01-06_17-52-46.gif

はじめに

あけましておめでとうございます!
2021新春アドベントカレンダー Tech Connectの3日目を担当します@usankusaiです。

突然ですが、皆さんは「日本全国酒飲み音頭」という曲をご存じでしょうか。
ご存じの方には言わずもがなとは思いますが、何かしらの理由をつけてお酒を飲めるぞ、という飲兵衛なら誰もが共感するであろう歌詞を耳に残る音頭に乗せた昭和の名曲の一つです。
この曲は1974年にバンド「バラクーダ」さんによってリリースされ大ヒットを記録しました。

我が家は年始に親戚一同で集まることが多いのですが、酔っ払った祖父が赤ら顔で両手を叩きつつこの曲を歌うため、私にとってはこの歌を聞くのがこの時期の風物詩となっています。
…が、今年はご存じのとおりコロナ禍ということもあり、年始の親戚の集まりは見送りになることが決まりました。

年始の風物詩が見送りになったところに今回のアドカレのお誘いを受け、居ても立っても居られずジェネレーターの開発を決めました。

要件

今回のシステムでは、原曲である日本全国酒飲み音頭の歌詞を元に、それっぽい歌詞を作り出すジェネレーターのwebサービスを目指しました。
原曲の歌詞は下記などをご参照ください。
日本全国酒飲み音頭(うたまっぷ.com)

歌詞を分析すると1番・2番はそれぞれ

1番:(1~12月)は(イベント※)で酒が飲めるぞ
2番:(都道府県※)は(郷土料理※)で酒が飲める(方言)
※の部分は例外あり。

といった構成になっています。

当初は上記を理想値として実装をしたかったのですが、最終的には

1番:(1~12月)は(イベント※)で酒が飲めるぞ → 【月別イベント版】
2番:(都道府県)は(郷土料理)で酒が飲めるぞ → 【郷土料理版】
※の部分は例外あり。

というところまでの実現となりました。

使用した技術について

今回はかねてから興味があったvue + vuetify + javascriptの構成で、勉強をしつつシステムを構築しました。

処理の大まかな流れについて

ジェネレーターの大まかな処理の流れは下記のとおりです。
 ①:初期表示時にaxiosでMediaWiki APIにHTTPリクエストを送信し、Wikipediaから情報を取得する。
 ②:①で取得した情報を元に画面を描画する。
 ③:「酒を呑む!」ボタンを押下したらリール回転を実行する。
 ④:(月別イベント版)イベントをクリックしたらwikipediaで記事のページを開く。 ※記事が存在しないケースあり
   (郷土料理版)wikipediaの記事の内容をから該当の料理の情報を表示する。
  

公開リポジトリ―

ソースは下記で公開しています。
https://github.com/usankusai-dev/sakenomi_public

大雑把に開発して後で綺麗にしようと思ったのですが、とてもそんな時間がなかったのでかなり汚いコードになっています…(言い訳)

ポイント

ここからは開発の際に時間が掛かった、
 ・月別イベント、都道府県別の郷土料理の情報の取得先について
 ・スロット機能について
上記2点について実装の解説を記載します。

・月別イベント、都道府県別の郷土料理の情報の取得先について

月別のイベント情報と都道府県別の郷土料理の情報は、MediaWiki ウェブサービス APIを利用し、wikipediaの記事からそれぞれ取得しています。
 -月別:年中行事
 -都道府県別:日本の郷土料理一覧

どちらも記事の情報自体はaction=queryを指定することで取得が可能です。
ただし今回使用した2つの記事はそれぞれ書式が異なるため、取得方法が異なっています。

月別で使用している年中行事のほうは、見出しが月別で分かれており、月内のイベントもプレーンテキストで記載されています。
このため記事のプレーンテキストを返すprop=extractsを指定することで情報を取得することができます。

APIサンドボックス(年中行事)

(年中行事の取得結果 ※内容の一部。実際だとマルチバイト文字はunicode形式となっています)
=== 四月 ===
稲荷祭 - 灌仏会 - 十三詣り - 入学式


=== 五月 ===
葵祭 - 白馬節会 - 神田祭 - こいのぼり - 三社祭 - 端午 - 運動会


=== 六月 ===
厳島神社例祭 - 山王祭 - 虫送り - 大祓

記事を見れば分かると思いますが、月ごとのイベントはそれぞれ個別の記事ページが用意されているものが多かったため、wikipediaのそれぞれの記事ページへのハイパーリンクを付与しています。
(記事がないイベントもあります)

一方で日本の郷土料理一覧の記事は都道府県ごとの料理が表形式で記載されています。
この場合、prop=extractsでは表内のデータを取得することができません。

APIサンドボックス(日本の郷土料理一覧) ※prop=extracts版

(日本の郷土料理一覧の取得結果[prop=extracts] ※内容の一部。実際だとマルチバイト文字はunicode形式となっています)
== 北海道地方 ==


=== 北海道 ===


== 東北地方 ==


=== 青森県 ===

このように記事中の表内のデータを利用したい場合は、記事の情報を返すprop=extractsではなく履歴情報を返すprop=revisionsを利用します。
このprop=revisionsの中に特定のリビジョンの記事内容を返すrvprop=contextというのがあり、これを指定すると表も含めたネイティブのテキストを取得することができます。
(ただし下記の通りwikipediaの記事ページを作成時のテキストとなるためそれなりに加工が必要となります)

APIサンドボックス(日本の郷土料理一覧) ※rvprop=context版

(日本の郷土料理一覧の取得結果[rvprop=context] ※内容の一部。実際だとマルチバイト文字はunicode形式となっています)
== 北海道地方 ==
{{seealso|Category:北海道の食文化}}
{{seealso|アイヌ料理}}
=== <!-- 01 -->北海道 ===
{|class="wikitable" style="font-size:smaller;"あ
|-
!料理名!!別名!!発祥時期!!地域!!料理概要!!出典!!備考
|-
||[[石狩鍋]]||秋味鍋||-||[[石狩地方]]<br />(沿岸地域)||土鍋に昆布のダシを取り、サケをぶつ切りにして野菜と共に煮込んだ物。<br />味噌仕立てと[[酒粕]]を入れる物がある。||<ref name="hiiragi">全国料理研究会柊会、『日本の郷土料理』、1974年、ドメス出版、東京</ref><ref name="okada_029">[[#岡田|岡田]]、p.29</ref><ref name="nousangyoson">「[http://www.rdpc.or.jp/kyoudoryouri100/recipe/menu_list/ 選定料理一覧]」『[[#農山漁村の郷土料理百選|農山漁村の郷土料理百選]]』</ref>||-
|-
||[[なんこ鍋]]||なんこう||-||[[空知地方]]||なんこ(馬の腸)を鍋に投入した味噌仕立ての鍋料理。もっぱら産炭地で鉱山労働者の間で広まり食べられていた。

・スロット機能について

「酒を呑む!」ボタンを押下した際のスロット機能はCSSを利用して実現しています。
下記はスロットのスタイルを一部変更した場合の描画になります。
2021-01-06_18-15-23.gif

このように、縦に並べた文字列の要素を下から上にアニメーション再生することでスロットのリールっぽい動きを再現しました。

スロットの内部的な動きとしては
 1:選択した月(都道府県)によって文字列の配列を作成
   (月を変更した場合はスロット用のdiv要素を初期化)
 2:「酒を呑む!」ボタンを押したら1の配列からスロット用のdiv要素を【1周分+】適当な位置まで最後尾から作成する
 3:スロット表示欄のtop要素を一番下まで移動し、top要素が0になるまでアニメーション移動をさせる
という流れになっています。
2の【】の部分ですが、この部分がないとアニメーションの移動数が少なかった場合、下記のような描画となり、あまりスロットっぽく見えなくなります。
2021-01-06_18-53-05.gif
このため必ず配列1周分を追加し、スロットっぽい動きをするように対応を行いました。

反省点

今回の開発の反省点ですが、一番はVue(Vuetify)をあまり有効活用できなかった点です。
正直なところ、今回の開発はVueやVuetifyを使わずともhtmlとjsで十分実装が可能でした。
(おそらくコードももっと簡潔になったと思います)

今回は学習目的もありVue+Vuefityの構成を選択しましたが、
 ・結局使ったのはcomponentやrouter機能のみ
 ・レガシーなCSSやHTMLの実装がほとんどで、レスポンシブ対応も行えていない
など、結果としてはあまりメリットのない構成となってしまいました。

特にこうしたネタ系のサービスはPC以外にスマホなどでも手軽に動かせてナンボだと思うため、
レスポンシブ対応は時間があればやってみたいです。

さいごに

いまだ猛威を振るうコロナにより、季節のイベントは行うことができず、郷土料理を食べに各地へ赴くこともできませんが、
このジェネレーターで出た結果をコロナ禍を乗り切った後の楽しみとして考えて頂ければ幸いです。

(余談)
私個人としてはコロナ禍による宅呑み機会の増加により、
むしろ飲酒量が増えた結果、見事自己歴代新記録となる高値の尿酸値を叩き出ました。
現在は禁酒の毎日を送っていますが、引き続き摂生に努めたいと思います…
(ただし、タイトル通りこのジェネレーターの開発が終わった日はとっておきのウィスキーを吞みました)

アドベントカレンダー4日目は週明け1/12(火)、担当は@ssscさんです。お楽しみに!


リンク情報システム株式会社では一緒に働く仲間を随時募集しています!
また、お仕事のご依頼、ビジネスパートナー様も募集しております。お気軽にご連絡ください。
Facebookはこちら

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

React Redux Hooks公式ドキュメント翻訳(概要編)

Reactアプリケーションの状態管理のためのOSSライブラリである、React Reduxのバージョン7.1.0で追加された、Hooks APIの公式ドキュメントを翻訳していきます。

2021/1/公開。原文リンクは以下。
・公式ドキュメント(React Redux Hooks):https://react-redux.js.org/api/hooks

今回の記事は概要編です。
スクリーンショット 2021-01-06 23.07.19.png

Hooks APIとは

Reactの新しいhooks APIはローカルなコンポーネントのstateをより簡単に扱うことができる関数コンポーネントです。

React Reduxは今回、高階コンポーネント(HOC)の一つである既存のconnect関数の代わりに、いくつかのhook APIを提供します。これらのAPIを使えば、connect関数であなたのコンポーネントをラップすることをせずとも、ReduxのStoreに登録し、Actionを送る(dispatchする)ことが可能になります。

Hooks APIはReact Reduxのバージョン7.1.0で追加されました。

React ReduxアプリでHooks APIを使う

connect関数と同様に、Hooks APIを使う前の準備として、コンポーネントの階層全体でReduxのStoreを使えるように、以下のようにProviderを設定する必要があります。

const store = createStore(rootReducer)

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)

この準備が整えばこれから紹介する、React Redux hooks APIの全てをあなたの関数コンポーネントの中で使用することができます。

次回はHooks APIの一つ、useSelector()について説明します。

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

「エンジニアに向いている人」はGitHub user内にどれぐらいいるのか

注意

  • この記事はとあるツイートを話題にしていますが、筆者にツイート内容、およびツイートされた方を蔑む意図は一切ありません。
  • 筆者は統計、およびプログラミングに関してはまだまだひよっこです。したがって誤った見解や拙いコードを書いている可能性がありますが、お気づきの際は是非コメントにてご指摘ください。

背景

あけましておめでとうございます!
正月休みもあけ仕事も始まりましたが、みなさまいかがお過ごしでしょうか。

さて、正月早々、とあるツイートが反響を呼びました。

プログラミングスクール通ってるかどうかとかどうでもよくて、この年末年始にコード全く書いていない人はエンジニア向いてないんじゃないですかね、それぐらい好奇心が必要な職業だと思うけど

賛否両論あったこのツイートですが、内容はさておき、私はあることが気にかかりました。

「果たしてこの年末年始、コードを書いた人はどのぐらいいるのだろうか」

本記事では上記の疑問を検証していこうと思います。

レギュレーション

もちろんすべてのエンジニアに「この年末年始、コード書きましたか?」ときいてまわるわけにはいきません。そこで本記事では以下のレギュレーションに基づいて「年末年始にコードを書いた人(=エンジニアに向いている人?)」を判定していこうと思います。

  1. コードを書いた日は必ずgithubに草が生えている」と考える。すなわち「年末年始にコードを書いた人」とは「年末年始にgithubに草が生えている人」とする。また現状デフォルトブランチにマージされておらず未反映の分は考慮しない。
  2. 年末年始は12/30 ~ 1/3 の5日間と定義し、この間に1度でもcontributeした人の割合を測定する。
  3. githubのアカウントは1人1つとし、アカウントは必ず使われているものとする(すなわち未使用垢やサブ垢などは考えない)

方針

上記のレギュレーションにもとづき、より具体的な方針を決めていきます。

サンプル数はどのぐらい取れば良いか

githubを利用しているdeveloperの数は2020年時点で5600万人を超えており[1]、全員を調べるにはかなりの労力がかかります。
そこで本記事では「ある程度正しい結果」を得ることを目標におき、githubのuserの中から一部をサンプリングして、サンプルから得られた結果を全体のものとすることにしました。

では、一体いくつのサンプルをとればその結果は「ある程度たしかである」といえるのでしょうか?
実は統計的には、母集団が100万人を超えていたとしても、384人分のデータがあれば、95%信頼できるデータがとれてしまうのです。[2]
本記事では400人分のgithub usersのデータをサンプリングして使うこととしました。

実装方針

1. github APIでusers一覧を取得

GithubのUsers APIを使って, githubのuser一覧を取得します。
Github Users APIではuserは最大100件ずつの取得しかできないため、100件ずつ取得するリクエストを計4回投げます
また、idは17027045(筆者のgithubアカウントにふられたid)から取得し、これを「無作為抽出されたデータである」と考えます。(超初期のgithubアカウントはgithub開発者のものや、すでに使われていないものが多いのではないかと考えたため)

2. 各userのcontributionsを解析

githubのcontributionのデータは以下のurlにアクセスすると取得することができます。

https://github.com/users/${user}/contributions

上記で取得できるページの、いわゆる「草」の部分をx-rayというスクレイピング用のライブラリを用いて解析することにしました。[2]

実装

実装方針に基づき、node.jsで以下のようなコードを作成しました。
http requestを投げる際にaxiosを、日付の比較の際にday.jsを、スクレイピングの際にx-rayを使用しています。

node.js
main()

async function main(){
    const myGitHubId=17027045
    //自分のgithubIdから400人分のgithub usersを取得
    users = await getGitHubUsers(myGitHubId)
    //400人中年末年始にコードを書いていた人の数を集計
    const appropriateEngineerNum = await getAppropriateEngineerNum(users)
    //結果
    console.log(`エンジニアに向いている人は${users.length}人中, ${appropriateEngineerNum}人です`)
}

//since番目から400人分のgithub usersを取得する
async function getGitHubUsers(since){
    const axiosBase = require('axios');
    const axios = axiosBase.create({
    baseURL: 'https://api.github.com',
    headers: {
        'Accept': 'application/vnd.github.v3+json',
    },
    responseType: 'json'  
    })

    const users = []
    for(let i = 1; i <= 4; i++){
            response = await axios.get('/users', {params: {since: since + i*100, per_page: 100}})
            users.push(...response.data.map(user => user.login))
    }
    return users
}

//引数のusersのうち、年末年始にコードを書いていた人数を算出する
async function getAppropriateEngineerNum(users){
    const Xray = require('x-ray')
    const x = Xray()
    let appropriateEngineerNum = 0
    for(user of users){
        const contributionUrl = `https://github.com/users/${user}/contributions`
        const contributions = await x(contributionUrl, 'rect', [ { count: '@data-count', date: '@data-date' }])
        const newyearHolidaysContributions = contributions.filter((contribution) => {
            return isNewyearHoliday(contribution.date) && contribution.count > 0
        })
        if(newyearHolidaysContributions.length > 0){
            appropriateEngineerNum ++
        }
    }
    return appropriateEngineerNum
}

//引数の日にちが年末年始かを判定する
function isNewyearHoliday(dateStr){
    const dayjs = require('dayjs')
    contributionDate = dayjs(dateStr)
    return dayjs('2020-12-29').isBefore(contributionDate) && dayjs('2021-01-04').isAfter(contributionDate)
}

結果

スクリーンショット 2021-01-06 22.28.51.png
ということで、「年末年始にコードを書いていた人」は 全体の2%という結果になりました。

補足

github profileのcotribution設定からは、private contributionの反映をオフにすることができます。したがって、この設定がオフになっており、かつプライベートリポジトリへのcontributionをした人はカウントされていません。

余談 ~「エンジニアに向いている人とは」~

最後に余談ではありますが、「どんな人がエンジニアにむいているのか」について私の見解をお話しします。

ツイッターやブログなどをみていると、「〇〇な人はエンジニアにむいていない」という意見が盛んに発進され、時に反発をうけているのを目にします。
私自身はこのどれもが「エンジニアに向いていない人」の条件となるとは思えません。なぜならまわりを見渡してみても「その条件を満たしてなくてもエンジニアとして働き、かつすごい技術を持っている方はたくさんいる」からです。(例えば上記の例に関しても、年末年始にコードを書いていなくともすごい技術力を持ったエンジニアはたくさんいます)。これらの意見を鵜呑みにし、自信を失ってしまうのはすごくもったいないことだと思います。

しかしながらその一方で「〇〇な人がエンジニアに向いていない」という意見は「自分はエンジニアである以上〇〇だけは譲れない」というプライドをもっていることの裏返しだとも思うのです。またそこに対して「それは違う」と感じてしまうのも自分の中でまた別のプライドがある証ではないでしょうか。

そして私はそのエンジニアとしてのプライドを持っている人こそが「エンジニアに向いている人」だと思うのです。

私自身はエンジニアとして「新しい技術に興味をもち」, 「コードを書くことを楽しむ気持ちを忘れない」ことをプライドとしてもっています。2021年もこの気持ちを大切に日々鍛錬していきたいものです。

参考文献

[1] he 2020 State of the OCTO—VERSE
[2] x-rayを使ってgithubの草の情報を取得する
[3] SurvayMonkey ~アンケートの標本サイズ~

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

【JavaScript】値のundefinedと空値チェックメモ

何故メモするの?

* kintoneのJavaScriptカスタマイズを行う際に、undefinedか空文字かのチェックをすることが多いことと。
* 今まで厳格な等価評価でundefined評価をしていたけどどうやら推奨されていないと知ったため。
詳細はMDNを参照

良く使用していたチェック

// if文
if(val === undefined){

}else if(val === ''){

}else{

}

// 三項演算子
val === undefined ? '' : val === '' ? '' : val;

なんで推奨されないのか。

undefinedは、グローバルスコープ内の変数だから。
予約語ではないため、上書きしたりしてグローバル汚染してしまう恐れがあるため。
なるほど。

@StormSurge様から以下の理由によりundefinedが書き換えられることはなく厳密等価演算子での比較も成立するとご指摘頂きました。保守性を考慮して以降のプログラムもval === undefinedの厳密な等価評価式に変更しました。以前記載していた評価式(val === void 0)はコメントアウトしています。

最近のブラウザー (JavaScript 1.8.5 / Firefox 4 以降) での undefined は、 ECMAScript 5 仕様により、設定不可、書込不可のプロパティとなります。 (そうでない場合でも、上書きは避けてください。)

(結局)どのようにしたの?

// 空値 or undefinedチェック
if (!checkValUndefined(val)) {
   // 空値 or Undefined時の対応
} else {
   // それ以外の時の対応
}

/**
  * 引数の値がundefinedか確認する。
  * @param {*} val - undefinedかチェックする値
  * @returns {string} - undefinedの場合:空値/それ以外の場合:チェックする値
  */
const checkValUndefined = (val) => {
   // return val === void 0 
   return val === undefined   
     ? ''
     : val;
};

「void 0」の「0」の部分は「0」でなくても良いそうです。「void」はあらゆる値に作用して、undefinedを返すらしいです。全く知りませんでした。

よくkintoneでフィールドに値を入れたり取得したりする際に使用しています。

今後

kintoneで推奨されていないけど、最低限名前空間オブジェクトにして全体反映のJavaScript置き場に上のプログラムを配置すると、どのアプリからでも参照できるようになるから使い勝手めっちゃ良くなる。他にも共通しているような関数はここにまとめちゃってしまった方が良いのかなとも思い始めた…。

終わりに

もっとこうしたら良いとか、そこおかしいのとか等あったら教えてください。

参考

 @quicksortJavaScript undefined値の判定

更新履歴

20201/1/7 @StormSurge様コメントの指摘からコードの判定条件の仕方を厳格な等価評価に変更

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

VRサーフィンデバイス『In The GreenRoom(ITGR=アイティーギア)』のプロトタイプを作成

誰もがGreenRoomを体験できるようにしたい

サーファーの中でも、選ばれ者しか入ることのできない神聖な場所『GreenRoom』。
それは、最高の天気と、最高の波が作るチューブの中にのみ出現する世界。

人生でこのGreenRoomを体験できる人は一握りしかいません。
僕が作りたいデバイスは、この世界を誰もが体験できるデバイス『In The GreenRoom(ITGR=アイティーギア)』です。
しかも、ただ360度の画像をVRで表現したものではありません。
このデバイスのためだけに撮影された画像を使い、これを不安定なサーフィンの板の上という状況を再現した上で、板の上にたちがるタイミングか適切かどうかを判定することで、より『リアル感』を追求したデバイスを作ることです。
今回は、このデバイスのプロトタイプを作成しました。
実物は以下の通りです。

今回のプロトタイプで目指したもの

今回の実現したいVRデバイスの肝は以下の3つです。
① このデバイス専用に撮影された360度画像
② 海の上のサーフィンの板の不安定感を擬似体験できる板デバイス
③ 音ゲーの様にテイクオフのタイミングが正しいかどうか判定できるプログラミング
①は現在撮影中、②にはすでにあるもので代用できるため、③について今後開発が必要になります。

そこで今回のプロトタイプでは、③について考えるべく、不安定な板の上でテイクオフ(板の上に立つ)のタイミングを感圧センサーで感知し、それを別のデバイスに通知することを目指して作成しました。
最終的なセンターの通知先は3D空間ですが、今回はLINE Notifyを通知先に設定しました。

また、VRで視界が奪われた状況で、テイクオフがしっかりできるかどうかも確認する必要があるため、Oculusを使用し、Youtbube360のサーフィン画像を出しながら、トライアルを行いました。

実際使用したデバイス

① Oculus quest
② Mr.Takeoff

③ 下記で作成した感圧センサーデバイス

最終的には下記の様にこれらを下記の様に配置しました。
IMG_2340.jpg

感圧センサーデバイス

感圧センサーデバイスについては下記の記事にコードを記載してありますので、ご確認ください。
感圧センサーを押すと、LINE Notifyに通知を出す方法のまとめ

いざ実験!

スタッフの石井君登場

自分が医療法人の理事長であることを利用し、当院スタッフの石井君に被験者になっていただきました。
パワハラじゃないですよ!
見てください、この屈託のない笑顔を!
IMG_2337.jpg

実験でわかったこと

① 圧センサーが小さすぎる
この部分を正確に踏むのは結構何度が高い

② Oculusで視界が見えないので、Mr.テイクオフのレベルが激ムズになる
事前にMr.テイクオフ自体の練習が数回必要
ワイプで画面の左上に自分の姿を上から見た様な映像を載せても良いかも

③ 動画と状況が合っていないといまいち気分が乗らない
これは専用の動画を撮影することで解決可能

今後も試作機を作って、問題点を洗い出して行こうと思います。

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

Denoの開発環境をVSCodeで作る

この記事は、「囲みマス育成会」のために書かれた記事ですが、Denoの開発環境をVSCodeで作りたい方全体を対象をしております。
さいごに囲みマスの説明を少しだけさせてくださいm(__)m

はじめに

今回の内容は、2021年1月4日時点での方法です。
Denoは発展途上のランタイムなので、より良い方法があるよというときにはコメントにてお知らせしていただけると嬉しいです。
また、VSCode、Denoはインストール済みという前提で話を進めていきますので、まだインストールしていない方は先に済ませておくことをお勧めします。

1.Deno拡張機能のインストール

VSCodeにはありがたいことに、既にDenoの拡張機能が用意されています。
Deno - Visual Studio Marketplace
image.png

Installを押してインストールしましょう。

2.Deno拡張機能を有効化

Deno拡張機能を使うためには有効化する必要があります。VSCode全体で有効化することもできますが今回は作業フォルダ内でのみDeno拡張機能を有効化します。

①左下の設定アイコンから「設定/Settings」をクリック(またはショートカットキーCtrl+,)。
image.png

②下の画像の赤線部分をクリックしてWorkspaceの設定画面に変更。
image.png

③上の検索窓から「Deno」と検索。すると、「Deno: Enable」という項目があると思うので、チェックを入れる。
image.png

3.デバッグ設定ファイルの作成

プログラムを途中で止めて確認をすることができるようにデバッグ設定ファイルを作っていきます。
既に、「.vscode」というフォルダが作られていると思うので、その中に以下に示すデバッグ環境用のファイル「launch.json」を作ってください。

launch.json
{
  "version": "0.2.0",
  "configurations": [{
    "name": "Deno",
    "type": "node",
    "request": "launch",
    "cwd": "${workspaceFolder}",
    "runtimeExecutable": "deno",
    "runtimeArgs": ["run", "--inspect-brk", "-A", "${file}"],
    "port": 9229,
    "outputCapture": "std"
  }]
}

4.プログラム実行

以上の手順でVSCodeの設定は終わりです。

試しにtest.tsを作成して、Deno.と入力してみてください。Deno拡張機能が有効化されていれば下のように補間の関数などがたくさん出てくると思います。
image.png

次は、サンプルコードを動かしてみましょう。test.tsの中身を全部消して、次のプログラムを書き込んでください。

test.ts
import { serve } from "https://deno.land/std@0.83.0/http/server.ts";
const s = serve({ port: 8880 });
console.log("http://localhost:8880/");
for await (const req of s) {
  req.respond({ body: "Hello World\n" });
}

恐らく初めは1行目のリンクのところに波線が引かれていると思います。
image.png
これは1度実行すると消えると思うので、さっそく実行していきます。F5キーを押すと実行できます。

デバッグコンソールに「http://localhost:8880」と出力されたら、そのアドレスにブラウザからアクセスしてみましょう。「Hello World」と表示されれば設定完了です!
image.png

「囲みマス」について

image.png

この記事は、「囲みマス」の育成会用に書かれた記事です。「囲みマス」とは、コロナウイルスの影響で中止になった高専プロコン2020競技部門を勝手に開催する会として発足したプロジェクトです。
この「囲みマス」ゲームシステム部分もDenoで書かれており、Githubにてソースコードが公開されています。

もし育成会以外でこの記事をご覧になられた方で、「囲みマス」に興味を持たれた方は、大会等も行う予定ですので是非ご参加ください!

Kakomimasu - Github
囲みマス Twitter

余談

DenoをVSCode全体で有効化しない理由

image.png

Not recommended in global configuration
"グローバル設定では推奨されません"

となっています。DenoはJavascript/Typescriptランタイムなので、グローバルで有効化してしまうと普通にJavascriptを書きたいときなどにもDeno拡張機能が働いてしまいます。なので、workspaceまたはフォルダ単位で有効化することをお勧めします。

Deno拡張機能について

今回使用した拡張機能は公式でサポートされており、次のGithubリポジトリにて管理されているようです。なので今後も安心して使っていけそうです。
denoland/vscode_deno

ちなみに、VSCode Marketplaceで「Deno」と検索すると、いくつか出てきますが、製作者が「denoland」でないもののほとんどは公式版が出る前に制作されたものです。
image.png
現在の公式版はjustjavacさんとaxetroyさんの拡張機能(現在はDeprecated、非推奨となっています)にインスパイアされたものだそうです。

This project was inspired by justjavac/vscode-deno and axetroy/vscode-deno. Thanks for their contributions.1

また、同じく公式の「denoland」が出しているDeno(Canary)は、Deno1.6から搭載されたLanguage Serverに対応した拡張機能でまだ開発中のものです。今回説明した方法と同じような形で使用できます。興味のある方は使用してみてください。

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

JavaScript 初級者から中級者へ(スコープ編)

スコープとは

実行中の式から値と式が参照できる範囲
→5種類存在する
・グローバルスコープ
・スクリプトスコープ
・関数スコープ
・ブロックスコープ
・モジュールスコープ

グローバルスコープ

var function などで値を定義した時にグローバルスコープに表示される

console.log(b);
console.log(window.b);
//windowオブジェクトは省略

window.d = 1;
let d =2;
console.log(d);

//let d =2 が出力される

console.log(b)
windowオブジェクトは省略

スクリプトスコープ

let const などで値を定義した時にスクリプトスコープに表示される

関数スコープとブロックスコープ

//関数スコープ
function a() {
    //関数の中にあるスコープ
    let b = 0;
    console.log(b);  
}
a();

//ブロックスコープ
if (true) {
    //ブロックスコープは{}の中
    var c = 1; //varはスコープを無視
    // function d() {
    //     console.log("d is called");
    // }

    const d = function () { //関数式だとスコープ外ではエラーになる
        console.log("d is called");
    }

    //d();  //スコープを無視

}
d();


console.log(c); 

0
Uncaught ReferenceError: d is not defined

レキシカルスコープ

ソースコードが記載してあること
コードを書く場所によって参照できる変数が変わるスコープ

1.実行中のコードから見た外部スコープ
2.どのようにしてスコープを決定するかという仕様

のことをいう。

コードを記述した時点で決定するため”静的スコープ”とも呼ばれる

//自身のスコープの外側は参照可能
let a = 2;           //グローバルスコープ a, fn1
function fn1() {     //関数スコープ(fn1) b, fn2
    let b = 1;
    function fn2(){   //関数スコープ(fn2) c
        let c = 3;
        console.log(b);
    }
    fn2();
}

fn1();

スコープチェーン

スコープが複数階層になっていること

// グローバルスコープとスクリプトスコープではどちらの値がとれるか
let a = 2;      //スクリプトスコープ ○  内側  
window.a = 4;   //グローバルスコープ ×  外側    
function fn1() {
    let a = 1;
    function fn2() {       //内側から外側へ
        console.log(a);

        if (true) {
            var a = 3;
        }

    }
    fn2();
}
fn1();

クロージャー

レキシカルスコープの変数を関数が使用している場合

function fn1() {     
    let b = 1;                  //レキシカルスコープ
    function fn2(){   
        console.log(b);        //レキシカルスコープへの参照を保持している  = クロージャー
    }
    fn2();
}

fn1();

クロージャーを使ってできること

プライベート変数の定義
ー外部からアクセスできない

increment();
increment();
increment();

function increment() {
    let num = 0;
    num = num + 1;
    console.log(num);
}

1
1
1
初期化され、3回1が出力される

let num = 0;
increment();
increment();
increment();

function increment() {

    num = num + 1;
    console.log(num);
}

1
2
3

しかしnumはどこからでも変更できてしまう
そこでクロージャー

function incrementFactory() {
    let num = 0;

    function increment() {
        num = num + 1;        //num参照可能
        console.log(num);
    }
    return increment;         //returnで関数を返す
}

//console.log(num); 

const increment = incrementFactory();

increment();
increment();
increment();
increment();

1
2
3
4

動的な関数の生成

function addNumberFactory(num) {
    function addNumber(value) {
        return num + value;
    }
    return addNumber;
}

const add5 = addNumberFactory(5); 
const add10 = addNumberFactory(10); //(value)
const result = add10(10); //(num)
console.log(result);

即時関数 (IIFE)

関数定義と同時に一度だけ実行される関数。

let result = 
(function(仮引数){
  return 戻り値;
})(実引数);

//実行結果が呼び出し元に戻される

function a() {
    console.log('called');
}

a();

//一度だけ使う 即時関数
(function() {
    console.log('called');
})();

// 上と同じ意味 
(a)();

// let c = (1 + 2) * 3;  //グループ化 ()
// console.log(c);

let b = (function () {

    let privateVal = 0;  //関数の中でしか使えない  一度だけ初期化される
    let publicVal = 10;  //関数の中以外も使える

    function privateFn() {
        console.log('privateFn is called');
    }
    function publicFn() {
        console.log('privateFn is called' + privateVal++);
    }

    return {
        publicVal,
        publicFn
    };

})()

b.publicFn(); //cのpublicに格納されるため外から呼ぶことができる


let c = (function (d) {
    console.log('called' + d);
    return 0;
})(10);
console.log(c);

called
called
called
privateFn is called0
privateFn is called1
privateFn is called2
privateFn is called3

----------------------------------------------------------------
----------------------------------------------------------------

まとめ問題

問題

スコープ

/**
 * 問題1:
 * 以下のコードではエラーが発生します。
 * コンソールで"fn called"と表示されるように
 * fn内のコードを変更してください。
 * 
 * ※if文は削除してはいけません。
 */
function fn() {
    if(true) {
        let a = 'fn called';
    }
    return a; // ReferenceError: a is not defined
}

const result = fn();
console.log(result);

/**
 * 問題2:
 * fn2内の記述を変更して、各コンソールで
 * 期待値を出力するように修正してください。
 */
var val = 'val1';
function fn2() {
    console.log(val); // 期待値->'val1'

    if(true) {
        var val = 'val2';
        console.log(val); // 期待値->'val2'
    }

    console.log(val); // 期待値->'val1'
}
fn2();

/**
 * 問題3:
 * 「クロージャー(プライベート変数)」のレクチャーで作成
 * したincrementと同じ機能を持つincrement関数をブロック
 * スコープとクロージャーを利用して作成してみてください。
 * 
 * increment(); // 期待値->1
 * increment(); // 期待値->2
 * increment(); // 期待値->3
 * increment(); // 期待値->4
 */


クロージャー

/**
 * 問題:
 * 四則演算を行うメソッドを持ったオブジェクトを
 * クロージャーを用いて作成してみてください。
 * 
 * 四則演算を行うメソッド(plus, minus, multiply, divide)
 * を実行すると以下のようにコンソールに出力されます。
 * 
 * const calc = calcFactory(10); // 初期値を10として設定
 * calc.plus(5);      // 出力結果 "10 + 5 = 15"
 * calc.minus(3);     // 出力結果 "15 - 3 = 12"
 * calc.multiply(3);  // 出力結果 "12 x 3 = 36"
 * calc.divide(2);    // 出力結果 "36 / 2 = 18"
 * 
 * ※前に行った計算結果をもとに四則演算を行います。
 * ※四則演算は"+","-","*","/"を数値ではさんで計算を行います。
 */



即時関数

/**
 * 問題:
 * クロージャーの問題で作成した以下のcalcFactoryを即時関数
 * で書き直してみてください。
 */
function calcFactory(val) {
    return {
        plus: function(target) {
            const newVal = val + target;
            console.log(`${val} + ${target} = ${newVal}`);
            val = newVal;
        },
        minus: function(target) {
            const newVal = val - target;
            console.log(`${val} - ${target} = ${newVal}`);
            val = newVal;
        },
        multiply: function(target) {
            const newVal = val * target;
            console.log(`${val} x ${target} = ${newVal}`);
            val = newVal;
        },
        divide: function(target) {
            const newVal = val / target;
            console.log(`${val} / ${target} = ${newVal}`);
            val = newVal;
        },
    }
}
const calc = calcFactory(10);
calc.plus(5);
calc.minus(3);
calc.multiply(3);
calc.divide(2);

答え

スコープ;答え

/**
 * 問題1:
 * 以下のコードではエラーが発生します。
 * コンソールで"fn called"と表示されるように
 * fn内のコードを変更してください。
 * 
 * ※if文は削除してはいけません。
 */
function fn() {
    let a = 'fn called';
    if(true) {
        a = 'fn called';
    }
    return a; // ReferenceError: a is not defined
}

const result = fn();
console.log(result);

/**
 * 問題2:
 * fn2内の記述を変更して、各コンソールで
 * 期待値を出力するように修正してください。
 */
var val = 'val1';
function fn2() {
    console.log(val); // 期待値->'val1'

    if(true) {
        let val = 'val2';
        console.log(val); // 期待値->'val2'
    }

    console.log(val); // 期待値->'val1'
}
fn2();

/**
 * 問題3:
 * 「クロージャー(プライベート変数)」のレクチャーで作成
 * したincrementと同じ機能を持つincrement関数をブロック
 * スコープとクロージャーを利用して作成してみてください。
 * 
 * increment(); // 期待値->1
 * increment(); // 期待値->2
 * increment(); // 期待値->3
 * increment(); // 期待値->4
 */

{       //クロージャー
    let num = 0;
    function increment() {
        num = num + 1;
        console.log(num);
    }
}

 increment();
 increment();
 increment();
 increment();

クロージャー ;答え

/**
 * 問題:
 * 四則演算を行うメソッドを持ったオブジェクトを
 * クロージャーを用いて作成してみてください。
 * 
 * 四則演算を行うメソッド(plus, minus, multiply, divide)
 * を実行すると以下のようにコンソールに出力されます。
 * 
 * const calc = calcFactory(10); // 初期値を10として設定
 * calc.plus(5);      // 出力結果 "10 + 5 = 15"
 * calc.minus(3);     // 出力結果 "15 - 3 = 12"
 * calc.multiply(3);  // 出力結果 "12 x 3 = 36"
 * calc.divide(2);    // 出力結果 "36 / 2 = 18"
 * 
 * ※前に行った計算結果をもとに四則演算を行います。
 * ※四則演算は"+","-","*","/"を数値ではさんで計算を行います。
 */


    function calcFactory(val) {         //calcFactory(10)<-val
        return {
            plus: function(target) {              //plus minus multiply divide 格納
                const newVal = val + target;      //calc.plus(5)<-target
                console.log(`${val}  + ${target} + ${newVal}`);
                val = newVal; //valはどこからでも使えるレキシカルスコープ
                              //newValを格納しないとすべて10になってしまう
            },
            minus: function(target) {              
                const newVal = val - target;      
                console.log(`${val}  - ${target} + ${newVal}`);
                val = newVal;
            },
            multiply: function(target) {              
                const newVal = val * target;      
                console.log(`${val}  * ${target} + ${newVal}`);
                val = newVal;
            },
            divide: function(target) {              
                const newVal = val / target;      
                console.log(`${val}  / ${target} + ${newVal}`);
                val = newVal;
            },

        };

 };


const calc = calcFactory(10);
calc.plus(5); 
calc.minus(3);
calc.multiply(3);
calc.divide(2); 

即時関数;答え

/**
 * 問題:
 * クロージャーの問題で作成した以下のcalcFactoryを即時関数
 * で書き直してみてください。
 */
const calc = (function(val) {
    return {
        plus: function(target) {
            const newVal = val + target;
            console.log(`${val} + ${target} = ${newVal}`);
            val = newVal;
        },
        minus: function(target) {
            const newVal = val - target;
            console.log(`${val} - ${target} = ${newVal}`);
            val = newVal;
        },
        multiply: function(target) {
            const newVal = val * target;
            console.log(`${val} x ${target} = ${newVal}`);
            val = newVal;
        },
        divide: function(target) {
            const newVal = val / target;
            console.log(`${val} / ${target} = ${newVal}`);
            val = newVal;
        },
    }
})(10);

calc.plus(5);
calc.minus(3);
calc.multiply(3);
calc.divide(2);
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

YoutubeのLive配信が予約されたら、Googleカレンダーに登録されるスクリプト

Vtuberにハマっていますが、好きなVtuberのLive配信の予定をGoogle カレンダーに自動で登録できたら、そのために予定を調整するのに便利だよなぁ、と思ってスクリプトを作ってみました。

Vtuberさんが協力を仰がなくても自動でGoogle カレンダーに登録できるものの、マメに配信枠を予約するVtuberさんじゃないと有効じゃないかも。

前提条件

Googleのアカウントを持っている
Google カレンダーの利用を開始している
Youtubeの利用を開始している
(たぶんPCのブラウザでしか使えないです)

機能

・任意のYouTubeのチャンネルがLive配信を予約したら、任意のGoogleカレンダーへ予定を登録する。
・カレンダーの予定の場所には配信のURL、メモには概要欄の内容を登録する。
・URLが同じ配信予約は重複して予定登録されない。

できない機能

・後から配信予約の時間が変更された場合、反映する機能はついてません。
・削除された場合も同様

スクリプト

let scheduleUpcomingLive=()=> {
  let results = YouTube.Search.list(
    'snippet', {
    channelId: 'CHANNEL ID', //CHANNEL IDを、予定を取りたいYoutubeのチャンネルのChannel IDに書き換えてください(URLから取れます)
    eventType:'upcoming',
    type:'video'
    }
    );

  let calendarId = 'YOUR CALENDAR ID';//YOUR CALENDAR IDを、予定を登録したいカレンダーのカレンダーIDに書き換えてください
  let now = new Date()
  let optionalArgs = {
    timeMax: (new Date(now.getTime() + (180 * 24 * 60 * 60 * 1000))).toISOString(),
    timeMin: (new Date(now.getTime() - ( 7 * 24 * 60 * 60 * 1000))).toISOString(),
    showDeleted: false,
    singleEvents: true,
  };
  let response = Calendar.Events.list(calendarId, optionalArgs);
  let events = response.items;

  for(let i in results.items){
    let resultsVideo = YouTube.Videos.list(
      'snippet,liveStreamingDetails',{
        id : results.items[i].id.videoId
        }
      )

    let VideoId = results.items[i].id.videoId

    let URLofstream = 'https://www.youtube.com/watch?v=' + VideoId + '&ab_channel=' + resultsVideo.items[0].snippet.channelTitle.replace(/\s+/g, "");

    let title = resultsVideo.items[0].snippet.title;
    let startTime = new Date(resultsVideo.items[0].liveStreamingDetails.scheduledStartTime);
    let endTime = new Date(resultsVideo.items[0].liveStreamingDetails.scheduledStartTime);
    endTime.setHours(endTime.getHours() + 1)
    let option = {
      description: resultsVideo.items[0].snippet.description,
      location: URLofstream
      }
      let arrayOfLocation = []

      for (let n in events) {
        arrayOfLocation.splice(n,0,events[n].getLocation())
      }

      if (arrayOfLocation.indexOf(URLofstream) < 0 ) {
        CalendarApp.getCalendarById(calendarId).createEvent(title, startTime, endTime, option);
        }
    }
  }

導入方法

便宜上、私がファンである黒乃くれはさんを例に説明します(ダイマ)。

黒乃くれはさんのチャンネル
https://www.youtube.com/channel/UCFVkfdFmaOh7BCtrqw8YvaA?sub_confirmation=1

Google Apps Scriptの準備

まずGoogle Apps Script (GAS)にアクセスします。
https://script.google.com/home

「新しいプロジェクト」をクリックします
image.png

プロジェクトが開くと、次のような画面が表示されるかと思います(旧エディター)
image.png

コードの内容を上記のスクリプトにすべて書き換えてください。
image.png

カレンダーIDと、YoutubeのチャンネルIDの部分を書き換えないといけないので、それぞれを取得する方法を記載します。

GoogleカレンダーIDを取得する。

(既存のカレンダーを利用したい場合は、歯車マークのとこまで飛ばしてください)
Google カレンダーを開きます。
https://calendar.google.com/

新しくカレンダーを作りたい場合

左のメニューから、+ボタン(他のカレンダーを追加)をクリックします。
1.png

表示されたドロップダウンメニューから、「新しいカレンダーを作成」をクリックします。
image.png

名前をわかりやすいものに変更し、「カレンダーを作成」をクリックします。
image.png

ここから、既存のカレンダー、新しいカレンダーどちらも手順は同じです

トップメニューに戻り、画面の右上にある歯車マークをクリックします。
表示されたドロップダウンメニューから「設定」クリックします。
image.png

左に表示されたメニューから、先ほど作成したカレンダーをクリックします。
2.png

カレンダーIDをコピーします。
3.png

GASに戻って、スクリプトの部分にコピペします。
4.png

YouTubeチャンネルIDの取得

配信予約を取得したいYoutubeのチャンネルページにアクセスします。
黒乃くれはさんの場合、次のURLです。
https://www.youtube.com/channel/UCFVkfdFmaOh7BCtrqw8YvaA

image.png

URLの末尾の部分(~channel/以降の部分)がチャンネルIDですので、コピーします。
(場合によってはチャンネルIDに続けて?sub_confirmation=1などが付いていることがありますので、それは除いてください)
image.png

上記のスクリプトのチャンネルIDの部分に、チャンネルIDをペーストします。
(クォーテーションマーク ' ' は消さないでください)
image.png

APIの準備

旧エディターの場合

GASでリソースのメニューから「Googleの拡張サービス」をクリックします。
image.png

初回だと、拡張サービスを利用するのに確認が入ります。
青字になっている「Cloud Console Terms of Service」をクリックしてください。
image.png

表示されたウェブページで、次のような確認が入りますので、規約に同意してよろしければチェックし、「同意して続行」をクリックします。
image.png

改めてGASに戻り、もう一度リソースのメニューから「Googleの拡張サービス」をクリックします。
image.png

APIの一覧が表示されます。
image.png

Calendar APIと、YouTube Data APIを有効にしてください。
image.png

image.png

新エディターの場合

新エディターの場合は、次のようなメニューになります。
左側のメニューの中にある「Service」の横の+ボタンをクリックしてください。
image.png

Google calendar APIと、Youtube Data APIをそれぞれAddしてください。
image.png

権限付与

保存ボタンをクリックしてください。
image.png

その後、実行ボタンをクリックしてください。
image.png

初めてスクリプトを動かすときは、次のような確認が入ります。
「許可を確認」をクリックしてください。
image.png

自分のアカウントを選択してください。
5.png

次のような警告が表示されます。
「詳細」をクリックしてください。
image.png

デベロッパーは自分ですが、自分を信頼できる場合、警告が「~(安全ではないページ)に移動」をクリックしてください。
6.png

次のようなメニューが表示されます。
「許可」をクリックしてください。
7.png

GASの画面に戻り、再度実行ボタンをクリックしてください。
image.png

Google カレンダーを表示して、配信予約が適切にGoogle カレンダーに登録されていることを確認します。
(そもそも配信予約がない場合、表示されません)
image.png

トリガーの設定

一定時間ごとに配信予約が新たに登録されていないかチェックするように設定します。
今回は15分おきにチェックするようにします。

チェックする時間の間隔は短くするほど素早くカレンダーに反映できますが、YouTube Data APIはアクセスできる回数に限度があるので、あまり細かくすると1日あたりの上限にすぐ到達してしまい、その日はそれ以降、チェックできなくなります。

手順

トリガーボタン(時計マーク)をクリックしてください。
image.png

新エディターの場合、次のように表示されるので、トリガーボタン(時計マーク)をクリックしてください。
image.png

次のようなメニューが表示されます。
右下のトリガーを追加をクリックしてください。
image.png

「イベントのソースを選択」の下のドロップダウンメニューを開きます。
image.png

「分ベースのタイマー」をクリックしてください。
image.png

次に「時間の間隔を選択(分)」の下のドロップダウンメニューを開きます。
image.png

「15分おき」をクリックしてください。
image.png

「保存」をクリックしてください。
image.png

以上で導入は完了です。

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

好きなYoutuberがLive配信を予約したら、Googleカレンダーに自動で予定を入れるスクリプト

Vtuberにハマっていますが、好きなVtuberのLive配信の予定をGoogle カレンダーに自動で登録できたら、そのために予定を調整するのに便利だよなぁ、と思ってスクリプトを作ってみました。

Vtuberさんに協力を仰がなくても自動でGoogle カレンダーに登録できるものの、早めに配信枠を予約するVtuberさんじゃないと有効じゃないかも。

前提条件

Googleのアカウントを持っている
Google カレンダーの利用を開始している
Youtubeの利用を開始している
(たぶんPCのブラウザでしか使えないです)

機能

・任意のYouTubeのチャンネルがLive配信を予約したら、任意のGoogleカレンダーへ予定を登録する。
・カレンダーの予定の場所には配信のURL、メモには概要欄の内容を登録する。
・URLが同じ配信予約は重複して予定登録されない。

できない機能

・後から配信予約の時間が変更された場合、反映する機能はついてません。
・削除された場合も同様

スクリプト

let scheduleUpcomingLive=()=> {
  let results = YouTube.Search.list(
    'snippet', {
    channelId: 'CHANNEL ID', //CHANNEL IDを、予定を取りたいYoutubeのチャンネルのChannel IDに書き換えてください(URLから取れます)
    eventType:'upcoming',
    type:'video'
    }
    );

  let calendarId = 'YOUR CALENDAR ID';//YOUR CALENDAR IDを、予定を登録したいカレンダーのカレンダーIDに書き換えてください
  let now = new Date()
  let optionalArgs = {
    timeMax: (new Date(now.getTime() + (180 * 24 * 60 * 60 * 1000))).toISOString(),
    timeMin: (new Date(now.getTime() - ( 7 * 24 * 60 * 60 * 1000))).toISOString(),
    showDeleted: false,
    singleEvents: true,
  };
  let response = Calendar.Events.list(calendarId, optionalArgs);
  let events = response.items;

  for(let i in results.items){
    let resultsVideo = YouTube.Videos.list(
      'snippet,liveStreamingDetails',{
        id : results.items[i].id.videoId
        }
      )

    let VideoId = results.items[i].id.videoId

    let URLofstream = 'https://www.youtube.com/watch?v=' + VideoId + '&ab_channel=' + resultsVideo.items[0].snippet.channelTitle.replace(/\s+/g, "");

    let title = resultsVideo.items[0].snippet.title;
    let startTime = new Date(resultsVideo.items[0].liveStreamingDetails.scheduledStartTime);
    let endTime = new Date(resultsVideo.items[0].liveStreamingDetails.scheduledStartTime);
    endTime.setHours(endTime.getHours() + 1)
    let option = {
      description: resultsVideo.items[0].snippet.description,
      location: URLofstream
      }
      let arrayOfLocation = []

      for (let n in events) {
        arrayOfLocation.splice(n,0,events[n].getLocation())
      }

      if (arrayOfLocation.indexOf(URLofstream) < 0 ) {
        CalendarApp.getCalendarById(calendarId).createEvent(title, startTime, endTime, option);
        }
    }
  }

導入方法

便宜上、私がファンである黒乃くれはさんを例に説明します(ダイマ)。

黒乃くれはさんのチャンネル
https://www.youtube.com/channel/UCFVkfdFmaOh7BCtrqw8YvaA?sub_confirmation=1

Google Apps Scriptの準備

まずGoogle Apps Script (GAS)にアクセスします。
https://script.google.com/home

「新しいプロジェクト」をクリックします
image.png

プロジェクトが開くと、次のような画面が表示されるかと思います(旧エディター)
image.png

コードの内容を上記のスクリプトにすべて書き換えてください。
image.png

カレンダーIDと、YoutubeのチャンネルIDの部分を書き換えないといけないので、それぞれを取得する方法を記載します。

GoogleカレンダーIDを取得する。

(既存のカレンダーを利用したい場合は、歯車マークのとこまで飛ばしてください)
Google カレンダーを開きます。
https://calendar.google.com/

新しくカレンダーを作りたい場合

左のメニューから、+ボタン(他のカレンダーを追加)をクリックします。
1.png

表示されたドロップダウンメニューから、「新しいカレンダーを作成」をクリックします。
image.png

名前をわかりやすいものに変更し、「カレンダーを作成」をクリックします。
image.png

ここから、既存のカレンダー、新しいカレンダーどちらも手順は同じです

トップメニューに戻り、画面の右上にある歯車マークをクリックします。
表示されたドロップダウンメニューから「設定」クリックします。
image.png

左に表示されたメニューから、先ほど作成したカレンダーをクリックします。
2.png

カレンダーIDをコピーします。
3.png

GASに戻って、スクリプトの部分にコピペします。
4.png

YouTubeチャンネルIDの取得

配信予約を取得したいYoutubeのチャンネルページにアクセスします。
黒乃くれはさんの場合、次のURLです。
https://www.youtube.com/channel/UCFVkfdFmaOh7BCtrqw8YvaA

image.png

URLの末尾の部分(~channel/以降の部分)がチャンネルIDですので、コピーします。
(場合によってはチャンネルIDに続けて?sub_confirmation=1などが付いていることがありますので、それは除いてください)
image.png

上記のスクリプトのチャンネルIDの部分に、チャンネルIDをペーストします。
(クォーテーションマーク ' ' は消さないでください)
image.png

APIの準備

旧エディターの場合

GASでリソースのメニューから「Googleの拡張サービス」をクリックします。
image.png

初回だと、拡張サービスを利用するのに確認が入ります。
青字になっている「Cloud Console Terms of Service」をクリックしてください。
image.png

表示されたウェブページで、次のような確認が入りますので、規約に同意してよろしければチェックし、「同意して続行」をクリックします。
image.png

改めてGASに戻り、もう一度リソースのメニューから「Googleの拡張サービス」をクリックします。
image.png

APIの一覧が表示されます。
image.png

Calendar APIと、YouTube Data APIを有効にしてください。
image.png

image.png

新エディターの場合

新エディターの場合は、次のようなメニューになります。
左側のメニューの中にある「Service」の横の+ボタンをクリックしてください。
image.png

Google calendar APIと、Youtube Data APIをそれぞれAddしてください。
image.png

権限付与

保存ボタンをクリックしてください。
image.png

その後、実行ボタンをクリックしてください。
image.png

初めてスクリプトを動かすときは、次のような確認が入ります。
「許可を確認」をクリックしてください。
image.png

自分のアカウントを選択してください。
5.png

次のような警告が表示されます。
「詳細」をクリックしてください。
image.png

デベロッパーは自分ですが、自分を信頼できる場合、警告が「~(安全ではないページ)に移動」をクリックしてください。
6.png

次のようなメニューが表示されます。
問題なければ「許可」をクリックしてください。
7.png

GASの画面に戻り、再度実行ボタンをクリックしてください。
image.png

Google カレンダーを表示して、配信予約が適切にGoogle カレンダーに登録されていることを確認します。
(そもそも配信予約がない場合、表示されません)
image.png

トリガーの設定

一定時間ごとに配信予約が新たに登録されていないかチェックするように設定します。
今回は15分おきにチェックするようにします。

チェックする時間の間隔は短くするほど素早くカレンダーに反映できますが、YouTube Data APIはアクセスできる回数に限度があるので、あまり細かくすると1日あたりの上限にすぐ到達してしまい、その日はそれ以降、チェックできなくなります。

手順

トリガーボタン(時計マーク)をクリックしてください。
image.png

新エディターの場合、次のように表示されるので、トリガーボタン(時計マーク)をクリックしてください。
image.png

次のようなメニューが表示されます。
右下のトリガーを追加をクリックしてください。
image.png

「イベントのソースを選択」の下のドロップダウンメニューを開きます。
image.png

「分ベースのタイマー」をクリックしてください。
image.png

次に「時間の間隔を選択(分)」の下のドロップダウンメニューを開きます。
image.png

「15分おき」をクリックしてください。
image.png

「保存」をクリックしてください。
image.png

以上で導入は完了です。

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

ES6 moduleのexportとimportについて

本日のゴール

  • export / import について理解を深める

アジェンダ

  • export について
  • import について

1. export について

変数、関数、クラスの宣言文の先頭に export を付けると、その名前で外部ファイルにエクスポートすることができる

export const foo = foo => {
  console.log()
}

export default class bar {
  constructor() {}
  init() {}
}

2. import について

外部モジュールや他のスクリプトなどからエクスポートされた関数、オブジェクト、プリミティブをインポートするために使用

import foo from './foo'
import {foo, bar} from './foo'

参考

MDN - export
MDN - import

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

非同期遷移に関する備忘録

当記事では、非同期遷移について調べたことなどをまとめています。

1. 非同期遷移とは

非同期遷移(あるいは非同期画面遷移・Pjaxとも)とは、ブラウザの履歴を操作するHistory APIやXMLHTTPRequestオブジェクトを利用して、画面を遷移する処理を指す言葉のようです。

「ようです」という曖昧な表現を使った理由は、この非同期遷移という言葉があまり一般的ではないためです。試しに、Googleで非同期遷移と検索をかけてみたところ、ヒットしたのは約12万件。これが非同期通信だと約87万件、さらにAjaxだと約2億6,300万件なので、その認知度の圧倒的な差が分かろうというものです。

しかしながら、では、一般的に使われていない処理ではないのかというと必ずしもそのようなわけではなく、YouTube・twitter・Facebook・GitHubなどの大手サイトを始めとした、少なくない数のWebサイトで実装されています。シームレスにページを遷移させるということに、わざわざ名前を付けるほどのこともないということなのかもしれません。

1-1. Ajaxとの違い

AjaxはXMLHTTPRequestオブジェクトを利用して非同期通信を実行し、WebサーバーからXML・HTML・jsonなどを得てWebページを非同期的に書き換える処理です。

非同期遷移はAjaxが技術的・思想的な土台となっています。非同期的にWebページを書き換えるのはAjaxそのものですが、そこにHistory APIを利用したブラウザ履歴の制御を加えることで、まるで同期的にページが遷移したように見せかけます。実際の操作感も同期遷移と大差ありません。ただし、非同期遷移は、より高速に、滑らかに、少ないデータ通信量で遷移を実現します。

1-2. Pjaxとの違い

Pjaxには2つの意味があります。1つは同期遷移の別表現、もう1つは同期遷移を実現するjQueryプラグインの名称です。なお、jQueryのプラグインとしてのPjaxには複数の実装が存在します。

2. Barba.js

非同期遷移を実現するためのライブラリとしてはBarba.jsが最有力候補となるでしょう。

barba.js

barbajs/barba: Create badass, fluid and smooth transition between your website's pages.

ところで、Barba.jsには旧バージョンと現バージョン(v2)の2種類があり、それぞれで処理の書き方が異なるらしいです。使用時・勉強時はバージョンの違いに注意してください。

3. 非同期遷移の考え方を順を追って理解する

ここからは非同期遷移を理解するために、非同期遷移処理を実装する過程を順々に記載します。完全に我流で書いていますので突っ込みどころが多いと思います。

以下、ソースコードです。

AGadget/asynchronous-transition-test: 非同期遷移処理の実装練習成果を置いています。

以下ページより動作確認できます。

トップページ - テスト

3-1. 非同期遷移の引き金

非同期遷移を実装するとき、まず考えるべきは遷移の引き金となる動作を決めることです。基本的には、同期遷移と同様に<a>タグをクリックしたときに非同期遷移させるのが無難でしょう。もちろん、任意の<div>タグや<span>タグをクリックしたり、マウスオーバーしたときなどに遷移させても問題ありません。

以下例では<a>タグをクリックしたときに非同期遷移するように処理を組んでいきます。

3-2. 同期遷移を中止させる

<a>タグをクリックしたときに非同期遷移させるならば、まず必要になるのは同期遷移を中止させる処理です。

main.js
window.addEventListener("load", () => {
  for (const anchor of document.querySelectorAll("a")) {
    anchor.addEventListener("click", (event) => {

      // 同期処理を止めます。
      event.preventDefault();
    });
  }
});

preventDefaultメソッドを使って、<a>タグをクリックしたときに同期遷移しないようにします。

Event.preventDefault() - Web API | MDN

JavaScriptのpreventDefault()って難しくない?preventDefault()を使うための前提知識 - Qiita

3-3. 遷移先のHTMLを読み込み置換する

同期遷移を止めたままページ内容を書き換えるためにXMLHttpRequestオブジェクトを利用して、非同期的に遷移先のページを取得・置換します。

遷移先のURLはクリックした<a>タグのhref属性から取得します。

置換対象は何らかの方法で指定してください。現在のHTMLと遷移先のHTMLとを突き合わせて、差異を調べ上げるみたいな処理を用意しても良いですが、処理を用意するのが面倒ですし、意図しない部分まで置換しそうで怖いです。定数などに格納しておくか、置換処理を関数に切り出して、引数で制御するのが無難でしょう。

XMLHttpRequest - Web API | MDN

main.js
window.addEventListener("load", () => {
  for (const anchor of document.querySelectorAll("a")) {
    anchor.addEventListener("click", (event) => {
      event.preventDefault();

      // 非同期的に遷移先のページを取得・置換します。
      // 今回は<title>タグと<main>タグを置換させてみます。
      const xhr = new XMLHttpRequest();
      xhr.onreadystatechange = () => {
        if (xhr.readyState === 4 && xhr.status === 200) {
          const temporaryWrapper = document.createElement("div");
          temporaryWrapper.innerHTML = xhr.responseText;
          for (const ELEMENT_TO_REWRITING of ["title", "main"]) {
            document.querySelector(ELEMENT_TO_REWRITING).innerHTML = temporaryWrapper.querySelector(ELEMENT_TO_REWRITING).innerHTML;
          }
        }
      };
      xhr.open("GET", anchor.href);
      xhr.send();
    });
  }
});

あるいはPromiseオブジェクトを利用して、取得処理と置換処理を区切ったほうが見やすいかもしれません。

main.js
window.addEventListener("load", () => {
  for (const anchor of document.querySelectorAll("a")) {
    anchor.addEventListener("click", (event) => {
      event.preventDefault();

      // 非同期的に遷移先のページを取得・置換します。
      new Promise((resolve) => {

        // 遷移先のページを取得します。
        const xhr = new XMLHttpRequest();
        xhr.onreadystatechange = () => {
          if (xhr.readyState === 4 && xhr.status === 200) {
            resolve(xhr.responseText);
          }
        };
        xhr.open("GET", anchor.href);
        xhr.send();   
      }).then((result) => {

        // 遷移先のページに置換します。
        const temporaryWrapper = document.createElement("div");
        temporaryWrapper.innerHTML = result;
        for (const ELEMENT_TO_REWRITING of ["title", "main"]) {
          document.querySelector(ELEMENT_TO_REWRITING).innerHTML = temporaryWrapper.querySelector(ELEMENT_TO_REWRITING).innerHTML;
        }
      });
    });
  }
});

3-4. ブラウザ履歴を操作

非同期書き換え処理に加えて、さらにブラウザ履歴の操作処理を実装します。具体的には、遷移前のページをブラウザ履歴に保存し、現在のURLを遷移後のページのURLに書き換えます。それぞれHistory APIのpushStateメソッドとreplaceStateメソッドを利用します。

History - Web API | MDN

main.js
window.addEventListener("load", () => {
  for (const anchor of document.querySelectorAll("a")) {
    anchor.addEventListener("click", (event) => {
      event.preventDefault();
      new Promise((resolve) => {
        const xhr = new XMLHttpRequest();
        xhr.onreadystatechange = () => {
          if (xhr.readyState === 4 && xhr.status === 200) {
            resolve(xhr.responseText);
          }
        };
        xhr.open("GET", anchor.href);
        xhr.send();   
      }).then((result) => {

        // 現在のページをブラウザ履歴に追加します。
        history.pushState(null, null, location.href);

        // 遷移後のページのURLに書き換えます。
        history.replaceState(null, null, anchor.href);

        const temporaryWrapper = document.createElement("div");
        temporaryWrapper.innerHTML = result;
        for (const ELEMENT_TO_REWRITING of ["title", "main"]) {
          document.querySelector(ELEMENT_TO_REWRITING).innerHTML = temporaryWrapper.querySelector(ELEMENT_TO_REWRITING).innerHTML;
        }
      });
    });
  }
});

ちなみに、実際に触ってもらえれば分かりますが、pushStateメソッドとreplaceStateメソッドの実行順は上記のようにする必要があります。この辺りを適当に弄ると、1度の移動で2つのページが履歴に登録されたり、遷移後のページが履歴に保存されたりと、おかしな挙動になるので注意が必要です。

3-5. ブラウザの進む・戻るに対応

最後に、ブラウザアプリに実装されている進む・戻る処理に対応します。この処理を忘れると<a>タグをクリックしての遷移はできますが、進む・戻るを実行してもブラウザ履歴が変化するだけでWebページが全く書き換わらなくなります。

ブラウザの進む・戻る処理はpopstateイベントと紐付けられています。このpopstateイベントに、非同期遷移用のイベントリスナーを追加します。イベントリスナーの内容は、これまでに実装した非同期通信処理がほぼ丸々流用できます。ただし、いくつか不要な処理があるので注意が必要です。

WindowEventHandlers.onpopstate - Web API | MDN

main.js
window.addEventListener("load", () => {
  // ※省略
});

// ブラウザの進む・戻る処理に対応した非同期遷移処理です。
window.addEventListener("popstate", () => {
  new Promise((resolve) => {
    const xhr = new XMLHttpRequest();
    xhr.onreadystatechange = () => {
      if (xhr.readyState === 4 && xhr.status === 200) {
        resolve(xhr.responseText);
      }
    };

    // 取得対象を変更します。
    // xhr.open("GET", anchor.href);
    xhr.open("GET", location.href);

    xhr.send();   
  }).then((result) => {

    // ブラウザ履歴を操作してはいけません。
    // history.pushState(null, null, location.href);
    // history.replaceState(null, null, anchor.href);

    const temporaryWrapper = document.createElement("div");
    temporaryWrapper.innerHTML = result;
    for (const ELEMENT_TO_REWRITING of ["title", "main"]) {
      document.querySelector(ELEMENT_TO_REWRITING).innerHTML = temporaryWrapper.querySelector(ELEMENT_TO_REWRITING).innerHTML;
    }
  });
});

ここで一旦、ソースコードを整理します。

main.js
/**
 * ファイルを取得する関数です。
 * 取得するファイルは第1引数で指定してください。
 * @param {String} filePath
 * @returns {Promise}
 */
const getFileByXMLHttpRequest = (filePath) => {
  return new Promise((resolve) => {
    const xhr = new XMLHttpRequest();
    xhr.onreadystatechange = () => {
      if (xhr.readyState === 4 && xhr.status === 200) {
        resolve(xhr.responseText);
      }
    };
    xhr.open("GET", filePath);
    xhr.send(); 
  });
};

/**
 * Webページを書き換える関数です。
 * 書き換え後のHTMLは第1引数で指定してください。
 * @param {String} HTMLString
 */
const rewritePage = (HTMLString) => {
  const temporaryWrapper = document.createElement("div");
  temporaryWrapper.innerHTML = HTMLString;
  for (const ELEMENT_TO_REWRITING of ["title", "main"]) {
    document.querySelector(ELEMENT_TO_REWRITING).innerHTML = temporaryWrapper.querySelector(ELEMENT_TO_REWRITING).innerHTML;
  }
};

/**
 * 非同期遷移処理です。
 * @author AGadget
 */

// <a>タグをクリックしたときの非同期遷移処理です。
window.addEventListener("load", () => {
  for (const anchor of document.querySelectorAll("a")) {
    anchor.addEventListener("click", (event) => {
      event.preventDefault();
      getFileByXMLHttpRequest(anchor.href).then((result) => {
        history.pushState(null, null, location.href);
        history.replaceState(null, null, anchor.href);
        rewritePage(result);
      });
    });
  }
});

// ブラウザの進む・戻る処理に対応した非同期遷移処理です。
window.addEventListener("popstate", () => {
  getFileByXMLHttpRequest(location.href).then((result) => {
    rewritePage(result);
  });
});

これで一応は完成ですが、他にも実装しておきたい機能があります。

3-6. 遷移アニメーション

UXを考えると、遷移時にはアニメーションを挟んだほうがよいでしょう。簡単ではありますがフェードイン・フェードアウトさせてみます。

main.js
/**
 * フェードインする関数です。
 */
const fadein = () => {
  const FADEIN_SECOND = 0.2;
  const fadeinTarget = document.querySelector("main");
  fadeinTarget.style.transition = `all ${FADEIN_SECOND}s`;
  fadeinTarget.style.opacity = 1;
};

/**
 * フェードアウトする関数です。
 * @returns {Promise}
 */
const fadeout = () => {
  return new Promise((resolve) => {
    const FADEOUT_SECOND = 0.1;
    const fadeinTarget = document.querySelector("main");
    fadeinTarget.style.transition = `all ${FADEOUT_SECOND}s`;
    fadeinTarget.style.opacity = 0;
    setTimeout(() => {
      resolve();
    }, FADEOUT_SECOND * 1000);
  });
};

/**
 * ファイルを取得する関数です。
 * 取得するファイルは第1引数で指定してください。
 * @param {String} filePath
 * @returns {Promise}
 */
const getFileByXMLHttpRequest = (filePath) => {
  // ※省略
};

/**
 * Webページを書き換える関数です。
 * 書き換え後のHTMLは第1引数で指定してください。
 * @param {String} HTMLString
 */
const rewritePage = (HTMLString) => {
  // ※省略
};

/**
 * 非同期遷移処理です。
 * @author AGadget
 */

// <a>タグをクリックしたときの非同期遷移処理です。
window.addEventListener("load", () => {
  for (const anchor of document.querySelectorAll("a")) {
    anchor.addEventListener("click", (event) => {
      event.preventDefault();
      Promise.all([fadeout(), getFileByXMLHttpRequest(anchor.href)]).then((result) => {
        history.pushState(null, null, location.href);
        history.replaceState(null, null, anchor.href);
        rewritePage(result);
        fadein();
      });
    });
  }
});

// ブラウザの進む・戻る処理に対応した非同期遷移処理です。
window.addEventListener("popstate", () => {
  Promise.all([fadeout(), getFileByXMLHttpRequest(location.href)]).then((result) => {
    rewritePage(result);
    fadein();
  });
});

フェードアウトアニメーションと遷移後のページ取得処理は並んで走らせておいたほうが、ページ遷移にかかる時間が小さくなるはずなのでPromise.allメソッドを使っています。

3-7. 別ページへのジャンプ時に非同期遷移を停止

これまでに仮定した状況では、<a>タグからは自サイトにしかジャンプしませんでしたが、現実的には別サイトのURLを指定することもあるはずです。ただ、他サイトに対して、非同期通信を試みるとブロックされることがありますし、何より他サイトに非同期遷移する必要もありませんので少し工夫します。

以下、ジャンプ処理部分を切り出しました。

// <a>タグをクリックしたときの非同期遷移処理です。
window.addEventListener("load", () => {
  for (const anchor of document.querySelectorAll("a")) {
    anchor.addEventListener("click", (event) => {
      event.preventDefault();
      Promise.all([fadeout(), getFileByXMLHttpRequest(anchor.href)]).then((result) => {
        history.pushState(null, null, location.href);
        history.replaceState(null, null, anchor.href);
        rewritePage(result);
        fadein();
      });
    });
  }
});

ジャンプ先が自サイトであるかどうか判定し、自サイトであるならば非同期遷移、自サイトでないならば普通に同期遷移します。判定処理は以下ブログの処理を使っています。

JavaScriptの正規表現でURLからドメイン・ホスト名を抽出する - Sarchitect

/**
 * 自サイトのURLかどうか判定する関数です。
 * 自サイトならばtrueを返します。
 * @param {String} URL
 * @returns {Boolean}
 */
const isMySite = (URL) => {
  if (location.host === URL.match(/^https?:\/{2,}(.*?)(?:\/|\?|#|$)/)[1]) {
    return true;
  }
};


// <a>タグをクリックしたときの非同期遷移処理です。
window.addEventListener("load", () => {
  for (const anchor of document.querySelectorAll("a")) {
    anchor.addEventListener("click", (event) => {

      // 判定します。
      if (!isMySite(anchor.href)) {
        return;
      }

      event.preventDefault();
      Promise.all([fadeout(), getFileByXMLHttpRequest(anchor.href)]).then((result) => {
        history.pushState(null, null, location.href);
        history.replaceState(null, null, anchor.href);
        rewritePage(result);
        fadein();
      });
    });
  }
});

3-8. その他考えられる機能

上記以外にも、リンク先にマウスオーバーした段階でページの取得処理を実行したりですとか、現在のWebページ中に表示されているリンク先全てのWebページを先んじて取得しておくなど、色々と面白い機能が思いつきます。

4. おわりに

非同期遷移は個人的にとても面白い題材です。「どのように書こうか?」「こういう機能はどうだろうか?」と考え、試行錯誤しながらガリガリ書くのはとても面白いです。

しかしながら、非同期遷移自体はブラウザに標準搭載されている遷移処理(同期遷移)をイチから再発明している側面があることも事実です。少なくとも非同期遷移は必須の技術・機能ではありません。あまり悪ノリに走り過ぎないように、自制の心を忘れないよう気を付けたいところです。

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

Markdown正規表現全文検索ツール「Note-CLI」をリリースしました。

Markdown正規表現全文検索ツール「Note-CLI」をリリースしました。

以下かんたんな日本語版クイックスタートになります。

Markdown Indexing and Pcre Regular Expression Compatible Full Text Searching for Advanced Note Takers.

ノートテイキング上級者のためのMarkdownインデックスとPCRE正規表現対応の全文検索システム。

クイックスタート

# データベースへMarkdownファイルをインデックス indexing file
./note-cli.js --index --database notes.db.example --file notes.md.example

# データベースで正規表現でキーワード検索 searching for keywords by regular expression
./note-cli.js --search --database notes.db.example --header "python" --content "hello.?world" --hide-sql

# 出力結果 and you will get:
[
    {
        "id": 2,
        "header": "how to hello world in python",
        "content": "```py \n# how to hello world in python\n\nprint(\"hello world\")\n```\n\n",
        "entire_note": "# how to hello world in python\n\n```py \n# how to hello world in python\n\nprint(\"hello world\")\n```\n\n"
    }
]

コンセプト Basic concept

There are great note taking tools over there, such as Evernote. But the reason I quit using Evernote was its poor searching capability (i.e. cannot search some special characters; cannot search with regular expression, of course) and also, lack of extensibility. That's where Note-CLI comes in.

I'm kind of person who likes to take a note on my favorite text editor (Atom editor, for my case). For note taking, I have a single Markdown format text file. In it, I do take notes about everything. Note-CLI does work for indexing chunks of Markdown content to a database file and enables you to search your Markdown notes with SQL query (and also regex).

巷には既に便利なノートテイキングツールが数多くあります。例えば僕はEvernoteの愛用者でしたが、Evernoteでは検索において特定の特殊文字を使えない、正規表現が使えない、など、欠点があります。オープンソースではないので、拡張性の低さもその一つです。そこにNote-CLIの活用場面があります。

僕は大抵のノートテイキングは、お気に入りのAtomエディタから、一つのMarkdown形式のファイル上で行います。そこにほぼ全ての知識があるわけです。Note-CLIはこのようなMarkdown形式で記述されたノートをパーシングしデータベースへインデックス、SQLおよび正規表現でのノートの検索を可能にします。

インストール Installation

git clone https://github.com/yuis-ice/note-cli.git
cd note-cli
chmod 755 ./note-cli.js # if needed
npm i

必要環境 Requirement

  • node.js v13.10.1 (probably higher or other versions work)
  • sqlite3-pcre
# node.js [nvm-sh/nvm](https://github.com/nvm-sh/nvm)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.37.2/install.sh | bash
bash
nvm install v13.10.1
node -v

# sqlite3-pcre
sudo apt update
sudo apt install sqlite3 sqlite3-pcre

例 Examples

https://yuis.xsrv.jp/images/ss/ShareX_ScreenShot_da0baae1-0746-49b4-ab0d-790b1adddb7c.png

./note-cli.js --search --database notes.db.example --header "python" --content "hello.?world" --limit 1 --hide-sql --format md | bat -l md --paging=never # using [sharkdp/bat](https://github.com/sharkdp/bat) *image
./note-cli.js --search --database notes.db.example --header "python" --content "hello.?world" --limit 1 --hide-sql --format json | jq ".[].header" -r # using [jq](https://github.com/stedolan/jq/)
./note-cli.js --search --database notes.db.example --header "python" --content "hello.?world" --limit 1 --hide-sql --format html | jq ".[].entire_note" -r
./note-cli.js --search --database notes.db.example --header "python" --content "hello.?world" --limit 1 --raw-sql "$( cat raw-sql.sql.example )"

コマンドラインオプション Command line options

$ ./note-cli.js --help
Usage: note-cli [options]

Options:
  -i, --index            set command type: index
  -f, --file <file>      (index) specify file to index (default: null)
  -s, --search           set command type: search
  -r, --regex            (search) enable regex extension for search (default: true)
  --header <keyword>     (search) search by header (default: ".")
  --content <keyword>    (search) search by content (default: ".")
  --note <keyword>       (search) search by note (default: ".")
  --limit <number>       (search) set limit for search (default: -1)
  -S, --raw-sql <sql>    (search) use raw SQL query for search (default: null)
  -H, --hide-sql         (search) disable showing sql query executed
  -F, --format <format>  (search) output format (default: "json")
  --pcre-path <file>     set sqlite3 pcre file path for search (default: "/usr/lib/sqlite3/pcre.so")
  -d, --database <file>  specify database file for index/search (default: "./note-cli.db")
  --delete-database      delete database
  -y, --yes              no confirmation prompt
  -h, --help             display help for command

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

【CSS・JS】テキストの選択を解除する方法。(文字列の選択を禁止する方法)

ドラッグである要素を選択したときに、その中の要素が選択状態になってしまう。この文字列の選択状態を解除する方法

例えば、テーブルのセルをドラッグで複数選択すると下図のように、セルの中のテキストが選択されて見づらい。

image.png

↓ 実現したいこと(文字列は選択しないようにする)

image.png

主な実現方法は以下2つ。1が圧倒的に簡単。2は参考程度に。

  1. cssでuser-selectプロパティの設定
  2. JacaScriptで選択解除


1. cssでuser-selectプロパティの設定

user-select: none;

対象の要素に上記を適用するとテキスト選択を解除できる。

コピペ防止でテキストの選択が全くできないページがあるが、その設定と同じ。

image.png

user-selectプロパティのその他オプション

user-selectプロパティは、none以外にも選択できる値がある。

概要 詳細
none 選択禁止 対象要素とその子孫要素の文章を範囲選択できなくする
all 全選択 どれか一つの文字を選択するとすべて選択する。
text テキスト選択可 デフォルトの状態
contain 要素の内部から選択 要素の内部から選択を始めることができます。しかし、選択範囲は要素の境界内のみに限定。
auto 要素の種類や親要素のプロパティにより変化 編集可能な要素の場合、使用値は containになる。親要素の user-select の使用値が all の場合、対象要素の使用値も all になる。など、

moz公式 user-select

▼user-select:all
image.png


2. JSで選択解除

以下処理を実行することで選択状態を解除できる。

const clearSelection = () => {
  //旧IE
  var sel
  if ( sel = document.selection ) {
      sel.empty();
  }
  //旧IE以外のブラウザ 
  else {
      //選択解除
      if (window.getSelection) {
          window.getSelection().removeAllRanges();
      }
      //入力中のテキストボックスの選択解除
      var activeEl = document.activeElement;
      if (activeEl) {
          var tagName = activeEl.nodeName.toLowerCase();
          if ( tagName == "textarea" ||
                  (tagName == "input" && activeEl.type == "text") ) {
              // Collapse the selection to the end
              activeEl.selectionStart = activeEl.selectionEnd;
          }
      }
  }
}

document.selection

旧IE(IE9〜10)のみ。ユーザーが選択した文字列範囲を指す。
IE11以降はwindow.getSelection()を使用。

window.getSelection()

ユーザーが選択した文字列範囲を指す。

if (window.getSelection){処理}は、選択範囲が存在するなら処理を実行。

empty()

emptyメソッドは指定した要素の子要素を削除。対象の要素自体は削除しない。

.removeAllRanges()

対象の選択範囲を解除する。
window.getSelection().removeAllRanges()ですべての選択範囲を解除。

document.activeElement

テキスト入力中の要素を返す。
inputタグやtextareaタグ内を選択している場合に、選択中の要素がわかる。

取得したデータの中には、対象のノード名やテキストの何文字目を入力中かなどのデータが入っている。

document.activeEl.nodeName.toLowerCase()

現在テキスト入力中のノード名(nodename)を取得し、小文字に変換(toLowerCase)する。

activeEl.selectionStart = activeEl.selectionEnd

選択中のテキストをなしにする。(始まり=終わりにする)

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

JavaScript 【最低限の文法】

 はじめに

こちらは学習用のメモになります。
今回は JavaScriptの基本的は基礎についてまとめていきたいと思います。

出力

console.logでJavaScriptの実行結果がわかります。

*実行結果を見る方法は実行したブラウザを左クリックして検証を押してConsoleを見ると実行結果がわかります。

index.js
console.log('Hello World');

①変数

index.js
let text = 'Hello World';

letは変数を宣言するもの。後ろの言葉を変数にする。

変数は書き換えることができます。

index.js
let text = 'Hello World';

//変数の更新
text = 'Hello world2';   

②定数

index.js
const bigText = 'Hello world';

定数は変数と似ているのですが、変数は上書きや更新ができるのですが定数は書き換えができないです。
つまり、定数は文字列を変えたくないときに使う。

*余談
・プログラミングでは単語が二つ重なる場合は、間の単語は大文字にするかアンダーバーを入れる。
・varでも変数を定義できるのですが最近だと変数と定数を使い分けるためにletconstを使い分けることが多い。

③配列

配列とは複数の文字列を入れて定義することができる。

index.js
let prefecture = ['山梨','東京','神奈川','埼玉'];

//一つ目の要素を取り出す
console.log(prefecture[0]);

*余談
プログラミングでは、カウントは0から始める

④ループ文(繰り返し)

ループ分は繰り返し処理をしたいときに使う。

1.for文

for文にはいつくか種類があります。
・for   繰り返しの回数を指定する場合に使う
・for in  現在はあまり使われていない
・for of (ES6から) 使いやすい

1) for of
index.js
const scores =[10,20,30];

// one of them(それらの中の一つ)
for(score of scores ){
  console.log(score);
}
2) for
index.js
// for(初期値;繰り返す条件;増減 ++ --)
for(let i=0;i<10;i++){
  console.log(i);
}

iが10以下なら繰り返す。

2.while文

下のサンプルではindexが5以下の場合、繰り返すループ文

index.js
//初期値
let index = 0;
//whileの()の中に条件式
while(index < 5){
//繰り返したい命令文
  console.log(index);
//indexの更新
  index++;
}

⑤条件分岐

1.if/else文

ifはもし〜だったらこうすると言った処理がしたいときに使う。

下のサンプルでは先ほど定義したprefecture.length(prefectureの要素の数)が3以上なら実行する。

index.js
//成立する条件
if(prefecture.length > 3){
//条件が満たされれば実行
   console.log(prefecture);
} else {
//条件が満たされなければ実行
   console.log('ないよ!');
}

2.swich文

if文と同じような意味。
現在はほとんどif文で書かれていることが多い。
理由はif文の方が使いやすく複雑にならないから。

index.js
const data =1;

switch(data){
  case 1:
      console.log('1です');
      break;
  case 2:
      console.log('2です');
      break;
  case 3:
      console.log('3です');
      break;
  default:   
      console.log('1-3ではありません');
      break;

  //注意 breakは必ず記述する

}

⑥関数

関数(function(機能))は、
・ 同じ命令を何度も使いたいとき
・ 任意のタイミングで命令を実行させたい時
に使います。

関数は2種類
・ 組み込み関数・・・準備してある関数
・ ユーザー定義関数・・・自由に作れる関数

下のサンプルでは先ほど定義したprefecture.length(prefectureの要素の数)が3以上なら実行する。

index.js
const test = () => {
   //ここに実行したい命令を書く
   if(prefecture.length > 3){
      console.log(prefecture);
   } else {
     console.log('ないよ!');
   }
};

//functionを用いた書き方(基本的には上の書き方が主流になっているみたいです)
// function test(){
//   if(scores.length > 3){
//     console.log(prefecture);
//   } else {
//    console.log('ないよ!');
//   }
// }

//こちらの記述で実行させる
test();

引数を使ったやり方

index.js
const test = (arg) => {
   if(prefecture.length > arg){
      console.log(prefecture);
   } else {
     console.log('ないよ!');
   }
};

//argに5が代入される
test(5);

⑦オブジェクト

オブジェクトは複数の値(変数/定数)や関数を持つことができる。

値(変数/定数)->プロパティ
関数->メソッド

index.js
const dora ={
  color: 'blue';
  size: 'large';
  purfume: 'mint';

  //関数も置くことができる
  eatSweets: () =>{
    console.log('どら焼きを食べた');
  }
};

//オブジェクトの中身を取り出す
console.log(dora);

//オブジェクトの中のcolorだけを取り出す
console.log(dora.color);

//オブジェクトの中の関数を取り出す
console.log(dora.eatSweets());

デフォルトで設定されているオブジェクトを呼び出す

1.window

windowはWebブラウザ全体のオブジェクト

index.js
//windowのプロパティ全てを取り出す
console.log(window);

//例えばブラウザの高さが知りたい時
console.log(window.innerHeight);

//ブラウザにポップアップを出すことができる
window.alert('Hello world');

2.document

documentはWebブラウザ上に表示されているページ全体のオブジェクト

index.js
//documentのプロパティ全てを取り出す
console.log(document);

//よく使うdocumentの関数
//HTMLの何かを指定・参照したいときに使う(buttonタグを呼び込む)
console.log(document.getElementsByTagName('button')[0]);
//[0]をを指定すると一つ目のbuttonタグ取得する

3.event

ユーザーがアクションしたタイミングで何かをしたいときに使う。

index.js
document.getElementsByTagName('button')[0].addEventListener('click', ()=> {
  //命令を書く
  //クリックした際にwindow.alertが発火される
  window.alert('Hello world');

});

//addEventListenerは引数を二つ指定できる
//一つ目の引数にはeventのタイプを入れる
//二つ目の引数には関数を指定する
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ボタンをクリックして要素を非表示から表示に切り替える

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8" name="Content-Style-Type" content="text/css">
    <title>Document</title>
</head>
<body>
    <h1 style="display: none;">サンプルタイトル</h1>
    <h2 style="display: none;">サンプルタイトル</h2>
    <button>ボタン</button>

    <script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
    <script>
        $('button').click(function() {
            $('h1').toggle();
            $('h2').toggle();
        })
    </script>
</body>
</html>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

園バス待ち中、忘れ物今取りに帰っても大丈夫?が目に見える仕組み!

幼稚園に子供を通わせて9年目、3児のママです。
幼稚園バスのあるあるだと思うのですが

・忘れ物!まだ取りに帰っても間にあう?
・出かけにモタモタ。バス行っちゃった?
・急にトイレ!家に一度帰っても大丈夫かな?

などなど、幼稚園バスの居場所がすぐにわかれば良いのにな~と思ったことが何度もあります。

知りたい時にバスの居場所がわかると

・雨の日、寒い日、終わりが見えずに待ち続けるという事がなくなり
・忘れ物など、一度家に取りに帰る事が出来たら、園まで後から送っていかなくても済んだり

メリットは沢山あると思います。

そこで、幼稚園バスの居場所をアプリのインストール不要!LINEからバスの位置情報を受け取れるサービスの開発をしていきたいと思います。

必要な技術など

・バスの位置を取得するGPSデバイス(今回はiphone)
・LINEで位置情報を取得する為のLINE Messaging API技術
・開発言語にはnode.jsを利用

位置情報を取得する

MDN WEB Docsを参考に位置情報の取得を行いました。

index.htmlとmap.jsファイル2つのファイルを作りました。

<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>位置情報取得</title>
    <script src="map.js" defer=""></script>
</head>

<body>
    <button id="find-me">Show my location</button><br/>
    <p id="status"></p>
    <a id="map-link" target="_blank"></a>
</body>

</html>
function geoFindMe() {

    const status = document.querySelector('#status');
    const mapLink = document.querySelector('#map-link');

    mapLink.href = '';
    mapLink.textContent = '';

    function success(position) {
        const latitude = position.coords.latitude;
        const longitude = position.coords.longitude;

        status.textContent = '';
        mapLink.href = `https://www.openstreetmap.org/#map=18/${latitude}/${longitude}`;
        mapLink.textContent = `Latitude: ${latitude} °, Longitude: ${longitude} °`;
    }

    function error() {
        status.textContent = 'Unable to retrieve your location';
    }

    if (!navigator.geolocation) {
        status.textContent = 'Geolocation is not supported by your browser';
    } else {
        status.textContent = 'Locating…';
        navigator.geolocation.getCurrentPosition(success, error);
    }

}

document.querySelector('#find-me').addEventListener('click', geoFindMe);

webブラウザで実装し、【Show my location】をクリックすると位置情報が取得でき
image.png

取得した位置情報のリンクをクリックすると地図が表示されました。位置あってる。
image.png

次に、取得したデータをデータベースへ保存

Firebaseを利用して、データを保存します
Firebase CloudFirestoreの使い方を初心者が解説してみた
今回は、このような形でデータベースを作成しました。
image.png

作成したデータベースにsetします。
setはデータベースのドキュメントの中身を上書きします。
参考にしたサイト
Firebase Cloud Firestoreのデータ更新 setとupdateの違い

await db.collection('map').doc('K9HEdPL7SQukhLGpIgLX').set({
  Latitude: position.coords.latitude,
  Longitude: position.coords.longitude
});

特定のドキュメントに位置情報を更新する仕組みは構築できました。
次に、

Firebaseの情報をLINEで取得

LINEの位置情報APIを利用して位置情報を送ります。
LINE 位置情報メッセージ
リッチメニューを作成して、リッチメニューを押したら位置情報を送るという仕組みにしました。

return client.replyMessage(event.replyToken, {
 type: 'location',
 title: '位置情報',
 address: 'Aバス',
 latitude: text.Latitude,
 longitude: text.Longitude
});

LINEの位置情報メッセージを利用すると、地図が出るのでとても分かりやすいですね。

image.png

最後に

デプロイ

位置情報取得ボタンのhtmlファイルは契約しているサーバーへアップロード
左端に位置情報のボタンがあるシンプルなものです。

image.png

LINE Messaging APIと連携をとっているnode.jsファイルはHerokuでデプロイしました。

実際に使ってみる

バスに乗って試す事は難しいので、今回は子供と使ってみました。
本来は15秒おきくらいに自動で位置情報を発信していきたいのですが、今回は手動でボタンを押したタイミングでの位置情報で実験をしていきます。

イマドコサーチで親から子供の居場所をいつもは調べるのですが、今回は特別に逆パターン。
意外と子供から聞かれる
「ママいつ帰ってくるの?」

これをLINEで解決してもらいます!

①まずはママがスマホを持ってスーパーへ出かける
②子供は好きなタイミングで【今どこ?】を押す
③帰宅後、LINEを見せてもらう

結果

「ママずっと同じ場所にいた!」

子どものLINEを確認すると、全て同じ地図の場所にピンがありました。
念のため、薬局で位置情報を確認したのですが、薬局に位置が変わっている~!と私は感動しました。

考察

実験をしてみて、子どものLINEでは居場所が変わらなかった

この件に関して、
Firebaseを確認したところ、最後にwebアプリ側から位置情報取得ボタンを押した緯度にちゃんとなっていました。
LINEを立ち上げっぱなしにしていた事により、最新のデータが取得できなかったのではないかな?と思います。

ボタンを押したタイミングではなく、リアルタイムで最新情報を取得する
今度の課題たと思いました。

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

jQueryとネイティブJavaScriptでスムーススクロール

webサイトにはかならずと言っていいほど実装されるスムーススクロール。
わたしもJavaScriptを勉強した当初真っ先に調べたコードでした。

そんなスムーススクロールを今回はjQuery版とJavaScript版のコード、どちらも書いていこうと思います。

どちらも前提にHTMLファイルにてリンク先はid付与しています

jQueryでスムーススクロール

$(function () {
// 対象となるセレクターを取得し、クリックイベントに無記名関数を代入
    $('a[href^="#"]').click(function () {
// href属性の属性値を取得
        let href = $(this).attr('href');
// 取得した属性値から飛び先のセレクターを取得(三項演算子をつかってます)
        let target = $(href == '#' || href == '' ? 'html' : href);
// 飛び先の位置を取得
        let pos = target.offset().top;
// bodyとhtmlにanimationプロパティを付ける
        $('body,html').animate({
            scrollTop: pos
        }, 400, 'swing');

        return false;
    });
});
  1. $('a[href^="#"]')で"#"からはじまるaタグをすべて取得します。
  2. クリックされた、$('a[href^="#"]')のhref属性の値を取得します。
  3. 三項演算子(条件式 ? true : false)をつかって飛び先のセレクターを取得し、変数targetに代入します。
  4. offset()メソッドを使用し、targetのtopからの位置を取得し変数posに代入します。
  5. bodyとhtmlにanimationプロパティを付けて完成!

JavaScriptでスムーススクロール

const a = document.querySelectorAll('a[href^="#"]');

for (let i = 0; i < a.length; i++) {
  a[i].addEventListener("click", (e) => {
    e.preventDefault();
    let href = a[i].getAttribute("href");
    let target = document.getElementById(href.replace("#", ""));
// 要素の上端のY座標を取得
    const rect = target.getBoundingClientRect();
// 現在のスクロール値を取得
    const set = window.pageYOffset || document.documentElement.scrollTop;
    const pos = rect.top + set;
    window.scrollTo({
      top: pos,
      behavior: "smooth",
    });
  });
}

手順としては同じです。
違うとすれば、
const rect = target.getBoundingClientRect();
const set = window.pageYOffset || document.documentElement.scrollTop;
const pos = rect.top + set;

の部分ですね。

getBoundingClientRect()はwindow上の座標の位置を返します。
rect.topで要素の上端のY座標を取得しています。

const set = window.pageYOffset || document.documentElement.scrollTop;
ではwindow.pageYOffsetがtrue(値あり)の場合、変数setに代入されますが
window.pageYOffsetに対応していないブラウザ用に後者のルート要素(html要素)のscrollTopを用意しています。

最終的な移動量はrect.topに現在のスクロール量を足したものとなります。
そうでないと、現在スクロールしている分だけ移動量が足りずスクロールが止まってしまいます。

あとはヘッダーの長さなどいろいろと各仕様に合わせて移動量を調整してもいいですね。

まとめ

個人的にはなるべくJavaScriptで書きたいですが、コードが短くなるjQueryはやっぱり魅力的ですね。
また、座標関係のメソッドやプロパティは基準値が色々あるので理解を深めていきたいです。

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

【JavaScript】エラー:Uncaught ReferenceError: Pace is not defined at

【JavaScript】エラー:Uncaught ReferenceError: Pace is not defined at

使用環境

  • XAMPP 8.0 (PHP 8.0)
  • windows10

背景

websiteを作成中に処理を共通化したらクリックイベントが発動しなくなった。あるページでは問題ないのに違うページでは動かない!なぜ?

エラー

エラー:Uncaught ReferenceError: Pace is not defined at

LoadingのライブラリPaceを使用していたため、それにまつわるエラーだということは分かった。

原因

外部JSファイルを読み込む順番だった。
外部JS→jQueryファイル→Paceファイルの順にファイルを読み込んでおり、Paceに関連する処理をJSファイルの1番上に持ってきていたため、JSファイルを読み込んだときに、Paceに関する処理がわからない!と怒られていた模様。

解決方法

外部JSファイルの読み込み順をLoading(Pace)ファイル→jQueryファイル→JSファイルの順番としたところ解決。

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

じゃんけんゲーム

ランダム関数の小数点以下を切り捨て、整数にする→Math.floor

var cphand = Math.random() * 3

これを

var cphand = Math.floor(Math.random() * 3)

とすることで「0,1,2」のみにする。

フォームのvalueを受ける

var myhand = document.getElementById('myhand').value

https://itsakura.com/js-selectbox

条件分岐
myhandとcphandを9回比較する。

参考サイト>https://myx.skr.jp/game0.html
http://www.ics.kagoshima-u.ac.jp/edu/katuyoukiso/vc/step4.html

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

クリックで指定要素が画面中央に来るようにスクロールする

概要

クリックで指定の要素が画面中央に来るようにスクロールする動きを実装したときのメモです。

今回はアンケートなどの「ラジオボタンでたくさん選択するようなフォーム」で、回答すると次の質問が画面中央にスクロールされていく、というものを作りました。

▼こんなかんじ
2c6f4e446e97d3d19f14aa5aef8c66a0.gif

実装

HTMLは以下のようになっています。

<li class="qs_col">~</li>が1つの質問のブロックになっているので、
「質問1のラジオボタンを選択すると、スクロールして質問2が画面中央に表示される」
ようにしてみます。

index.html
<form method="POST" action="">
  <li class="qs_col">
    <h2>質問1.~~~~~~~~~~ですか?</h2>
    <input type="radio" id="qs01_01" name="qs01" value="0">
    <label for="qs01_01">はい</label>
    <input type="radio" id="qs01_02" name="qs01" value="1">
    <label for="qs01_02">いいえ</label>
  </li>
  <li class="qs_col">
    <h2>質問2.~~~~~~~~~~ですか?</h2>
    <input type="radio" id="qs02_01" name="qs02" value="0">
    <label for="qs02_01">はい</label>
    <input type="radio" id="qs02_02" name="qs02" value="1">
    <label for="qs02_02">いいえ</label>
  </li>
  <li class="qs_col">
    <h2>質問3.~~~~~~~~~~ですか?</h2>
    <input type="radio" id="qs03_01" name="qs03" value="0">
    <label for="qs03_01">はい</label>
    <input type="radio" id="qs03_02" name="qs03" value="1">
    <label for="qs03_02">いいえ</label>
  </li>
  <div class="f_btn">
    <button type="submit">送信</button>
  </div>
</form>

 
スクロールを実装します。
フォームはラジオボタンのみなので、クリックを検知してINPUTタグであれば処理を実行します。

functionを作ってinput側にonclickを記述するのも考えましたが、実際にはラジオが50個以上あり、全てに入れるのが嫌だったのでaddEventListenerでのイベント検知にしています。

scroll.js
window.addEventListener('click', (event) => {
    // クリックした要素のタグを取得
    var tag = event.target.tagName;

    if (tag == 'INPUT') {
        // クリックした質問の次のli要素を取得
        var id = event.target.id;
        var element = document.getElementById(id).closest('li.qs_col').nextElementSibling;

        // 最後の質問は次のliが無いので送信ボタンエリアを取得
        if (element == null) {
            element = document.querySelector('.f_btn');
        }
        // 要素が画面中央に来るようにスクロール
        element.scrollIntoView({behavior: 'smooth', block: 'center'});
    }
});

これで、ラジオボタンで回答をクリックすると次の質問ブロックにスクロールします。

解説

今回はスクロールにElement.scrollIntoView()というメソッドを使いました。
scrollIntoView()が呼び出された要素がユーザーに見えるところまで、要素の親コンテナーをスクロールする、というものです。

最初は次のスクロール位置をoffset().topなどで取得してscrollTop()を使おうとしたのですが、「要素が画面中央に来るようにスクロール」するためには位置計算などが必要になります。

Element.scrollIntoView()ではそのような計算を使わず、簡単に位置指定することができます。

Element.scrollIntoView()

Element.scrollIntoView()には3つのオプションがあります。

  • behavior:アニメーションの設定
  • block:(通常は)垂直方向のスクロール位置設定
  • inline:(通常は)水平方向のスクロール位置設定

詳細な設定についてはこちらで詳しく解説されています。

// オプションの設定
element.scrollIntoView({behavior: "smooth", block: "end", inline: "nearest"});

// 今回の場合(アニメーションあり、中央配置)
element.scrollIntoView({behavior: 'smooth', block: 'center'});

今回は画面中央でしたが、上端・下端に合わせることも可能です。

これで実装は以上です!

参考

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

斎藤さんゲーム

saitousa Diagram.png

ランダム関数を使って
・0から3の時→何もなし
・3から6の時→ぺっぺぺー
・6から10の時→斎藤さんだぞぉ

をアラートで表示

こちらから遊べます!

https://u-hoshi.github.io/saitousanGame/

ソースコードはこちら
https://github.com/u-Hoshi/saitousanGame

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

ReactでCan't perform a React state update on an unmounted component.の原因と解決

プログラミング開始7ヶ月目の者です。

ReactのクラスコンポーネントのcomponentDidMountについてです。

メソッド内でsetTimeoutの記述をしました。

  // 表示される毎に実行
  componentDidMount() {
    setTimeout(() => {
      this.setState({ isVisible: !this.state.isVisible });
    }, 1000);
  }

それでsetTimeoutで値が切り替わる前に別のLink(Router)に飛ぶとエラーが出ました。

エラー

Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.

原因

componentDidMountしたのにunmountedされたまま、別のLinkに移った為です。
「マウントされないcomponentでstateを更新しようとしています。componentWillUnmountでキャンセルしてね。」

解決

componentWillUnmount用に変数とメソッドを追加し代入する。
これで別のページに移ったときにcomponentWillUnmountでキャンセルされます。

  // unmount用の変数
  handle;

  // 変数に代入
  componentDidMount() {
    this.handle = setTimeout(() => {
      this.setState({ isVisible: !this.state.isVisible });
    }, 1000);
  }
  // unmount用メソッドを追加 引数に変数を入れる。
  componentWillUnmount() {
   clearTimeout(this.handle);
  }

修正サンプル。
const props = {
  visible: {
    opacity: 1,
  },
  hidden: {
    opacity: 0,
  },
}
const Box = posed.div(props)

export default class Can extends React.Component {

  handle;

  state = {
    isVisible: false,
  };
  // mount
  componentDidMount() {
    this.handle = setTimeout(() => {
      this.setState({ isVisible: !this.state.isVisible });
    }, 1000);
  }
  // unmount
  componentWillUnmount() {
   clearTimeout(this.handle);
  }
  render() {
    return (
      <Box pose={this.state.isVisible ? 'visible' : 'hidden'}>出現</Box>
    );
  }
}

参考

エラーで悩んでいた時に見つけた 記事 です。

以上です!

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

Djangoで時刻をユーザーの地域(UTC)に合わせて表示させる

django-tz-detect を使う。 (ソースコードはこちら
2019年にリリースされたオープンソースライブラリで、MITライセンス適用済。
<実行環境>
Python:3.7.4 Django:2.2.17

1.ライブラリのインストール

pip install django-tz-detect

2.setting.pyを編集

INSTALLED_APPSの末尾に以下を追加

setting.py
INSTALLED_APPS (
    'tz_detect',#追加
)

TEMPLATESに'django.template.context_processors.request'があるか確認(なければ追加)

setting.py
TEMPLATES = [
    {
        ...
        'OPTIONS': {
            'context_processors': [
                ...
                'django.template.context_processors.request',#なければ追加
            ],
        },
    },
]

MIDDLEWAREの末尾にtz_detect.middleware.TimezoneMiddlewareを追加

setting.py
MIDDLEWARE = (
    'tz_detect.middleware.TimezoneMiddleware',#追加
)

3.url を追加する

urls.py
urlpatterns = [
    url(r'^tz_detect/', include('tz_detect.urls')),#追加
]

4.HTMLの編集

先頭に{% load tz_detect %}、bodyタグ内に{% tz_detect %}を追加

この時、{% tz_detect %}は読み込みの関係で</body>の直前に記述するのが望ましい。

base.html
{% load tz_detect %}
<head></head>
<body>
{% tz_detect %}
</body>

タイムゾーンの設定を変更して、ブラウザを上げなおして完成。
(反映まで1~2分かかることもある)

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

最小二乗法を用いた3次Bezier曲線あてはめ JavaScriptで

概要

最小二乗法を用いて順番に並んだ4つ以上の点を3次Bezierを用いて曲線あてはめをしてみます。

結果

黒い点はクリックした点、赤い点は求められた3次Bezierのコントロールポイントです。

  • 4点を指定したところ

スクリーンショット 2021-01-04 20.59.43.png

  • 7点を指定したところ

スクリーンショット 2021-01-04 20.59.54.png

プログラム

BezierCPs.js
export const
BezierCPs = _ => {
    const l = _.length - 1
    const s = _[ 0 ]
    const e = _[ l ]

    let pp = 0, pq = 0, qp = 0, qq = 0, p = 0, q = 0
    for ( let $ = 1; $ < l; $++ ) { //  Bypass first and last element, because 't' or 'u' will be zero.
        const o = _[ $ ]
        const t = $ / l
        const u = 1 - t
        const P = 3 * u * u * t
        const Q = 3 * u * t * t
        pp += P * P
        pq += P * Q
        qp += Q * P
        qq += Q * Q
        const PQ = o - u * u * u * s - t * t * t * e
        p += P * PQ
        q += Q * PQ
    }

    const dm = pp * qq - pq * qp

    return [
        + p * qq / dm - q * pq / dm
    ,   - p * qp / dm + q * pp / dm
    ]
}
index.html
<!DOCTYPE html>
<html lang=zxx>
<title>Bezier - LeastSquares</title>

<canvas id=Canvas width=500 height=500 style="border: 1px solid black"></canvas>
<br>
<button id=Clear>CLEAR</button>

<script type=module>

const
C2D = Canvas.getContext( '2d' )

import { BezierCPs } from './BezierCPs.js'

const
$ = []

const
Draw = () => {
    C2D.clearRect( 0, 0, Canvas.width, Canvas.height )
    C2D.fillStyle = 'red'
    if ( $.length > 3 ) {
        C2D.beginPath()
        C2D.moveTo( ...$[ 0 ] )
        const xPQ = BezierCPs( $.map( _ => _[ 0 ] ) )
        const yPQ = BezierCPs( $.map( _ => _[ 1 ] ) )
        C2D.bezierCurveTo( xPQ[ 0 ], yPQ[ 0 ], xPQ[ 1 ], yPQ[ 1 ], ...$[ $.length - 1 ] )
        C2D.strokeStyle = 'red'
        C2D.stroke()
        C2D.fillRect( xPQ[ 0 ] - 2, yPQ[ 0 ] - 2, 4, 4 )
        C2D.fillRect( xPQ[ 1 ] - 2, yPQ[ 1 ] - 2, 4, 4 )
    }
    C2D.fillStyle = 'black'
    $.forEach( _ => C2D.fillRect( _[ 0 ] - 2, _[ 1 ] - 2, 4, 4 ) )
}

Clear.onclick = () => ( $.length = 0, Draw() )
Canvas.onclick = ev => ( $.push( [ ev.offsetX, ev.offsetY ] ), Draw() )

</script>

解説

まずは2次元で考えます。

  • 始点を $(s_x,s_y)$

  • 制御点1を $(p_x,p_y)$

  • 制御点2を $(q_x,q_y)$

  • 終点を $(e_x,e_y)$

とする2次元の3次ベジエ曲線は媒介変数をtとする以下の式で表されます。

\left[ \begin{array}{c} x \\ y \end{array} \right]=\left[ \begin{array}{c} (1-t)^3s_x+3(1-t)^2t p_x + 3(1-t)t^2 q_x+t^3e_x \\ (1-t)^3s_y+3(1-t)^2t p_y+3(1-t)t^2 q_y+t^3e_y \end{array} \right],\; t=0…1

以下のような関数を用意すると

bz(t,s,p,q,e)=(1-t)^3s+3(1-t)^2t p + 3(1-t)t^2 q+t^3e

このようにも書けます。

\left[ \begin{array}{c} x \\ y \end{array} \right]=\left[ \begin{array}{c} bz(t,s_x,p_x,q_x,e_x) \\ bz(t,s_y,p_y,q_y,e_y) \end{array} \right],\; t=0…1

最小二乗法で求めるわけですからまずこれの誤差関数を求めてみましょう。
まず4点がクリックされた場合を考えてみます。4点をクリックされた順にcs, c1, c2, ceとします。計算で2つの点d1,d2を求めると誤差は以下のように考えられます。

||\vec{c_1d_1}||+||\vec{c_2d_2}||

最小二乗法では誤差の大小だけが重要になるので、扱いやすくするためにこれを二乗して2で割ります。以下のようになります。

\frac{{(d1_x-c1_x)}^{2} + {(d1_y-c1_y)}^{2} + {(d2_x-c2_x)}^{2} + {(d2_y-c2_y)}^{2}}{2}

d1, d2 を求めるにはtを以下のようにして3次ベジエの式にあてはめます。

t = \frac {1}{3}\ ,\frac {2}{3}\

tの決め方は他にもありますが、だいたいこれで十分な結果が出ます。

n点クリックされた場合に一般化しましょう。

J = \sum_{i=1}^{n-2}\frac 1{2}(bz(\frac i{n-1},s_x,p_x,q_x,e_x)-ci_x)^2+\sum_{i=1}^{n-2}\frac 1{2}(bz(\frac i{n-1},s_y,p_y,q_y,e_y)-ci_y)^2

Σの中をpx,qx,py,qyについて微分します。px,qx と py,qy は排他なので、結局関数bzとciについて微分すればよいです。

(bz(t,s,p,q,e)-c)^2

u を ( 1 - t )としてbzを展開すると

Diff(c,t,s,p,q,e) = (u^3s+3u^2t p+3ut^2q+t^3e-c)^2

ここで

A=u^3s,B=3u^2t,C=3ut^2,D=t^3e,E=-c

とおくと

Diff(c,t,s,p,q,e) = (A + Bp + Cq + D + E)^2
\\
= B^2p^2 + C^2q^2 + 2BCpq + 
\\ 2ABp + 2ACq + 2BDp + 2BEp + 2CDq + 2CEq + 
\\ A^2 + D^2 + E^2 + 2AD + 2AE + 2DE

pとqで偏微分します。ついでに2で割っておきます。

\frac{\partial Diff}{\partial p}=B^2p + BCq + AB + BD + BE 
\\
\frac{\partial Diff}{\partial q}=BCp + C^2q + AC + CD + CE

A,B,C,D,Eを戻すと

\frac{\partial Diff}{\partial p}
=9u^4t^2p + 9u^3t^3q + 3u^5ts + 3u^2t^4e - 3u^2tc
\\=(3u^2t)( 3u^2tp + 3ut^2q + u^3s + t^3e - c )
\\
\\
\frac{\partial Diff}{\partial q}
=9u^3t^3p + 9u^2t^4q + 3u^4t^2s + 3ut^5e - 3ut^2c
\\=(3ut^2)( 3u^2tp + 3ut^2q + u^3s + t^3e - c )

最後にΣの分だけ係数を足し込んでp,qについて連立方程式をといて返しています。

最後に

3次元にしたい場合は、単にz座標についてBezierCPsを呼べばいいはずです。

3次ベジエ曲線のあてはめをやる場合、接線接続をするために始点か終点あるいは両方のタンジェントを決めたい場合があります。
その場合は
https://github.com/soswow/fit-curve
とかがあります。

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

[JavaScript] Promise.all() の使いかた

Promise.all()はPromiseオブジェクトを要素に持つ配列を引数にとり、すべてのPromiseオブエジェクトが解決するまで処理を待つ。

Promise.all([promise1, promise2, promise3]).then((value)=> {
  // すべてのpromiseが解決した後に行う処理
  // value = [promise1の結果, promise2の結果, promise3の結果]
});

map()メソッドやpush()メソッドと組み合わせて使う例

// Promiseを要素に持つ配列を作る
const promises = array.map((a) => {
  // Promiseオブジェクトをreturnする
});

// 配列にPromiseを追加する
promises.push(/*他のPromiseオブジェクト*/)

Promise.all(promise).then((value)=> {
  // すべてのPromiseが解決した後に行う処理
});
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む