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

はじめてのThree.js 5章 日記

はじめてのthree.js

新たに知った知識

  • 2dオブジェクトに穴を開ける発想
  • thetaで結構変形できる
  • 片方を負の値でシリンダー作ると杯みたいな形ができるScreen Shot 2020-05-28 at 23.19.20.png
  • torusknotのpとqはそれぞれ結び目が軸の周りに何回巻きつくかと内側の穴の周りに何回巻きつくか(画像はp=9, q=11 のエンゼルフレンチ) Screen Shot 2020-05-28 at 23.26.57.png

気づいたこと

  • thetaのguiの値のmaxがなぜ6.3なのかと思ったけど単純に2Π

wow moment

  • エンゼルフレンチ

まだ解決していない点

6章はこちら

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

JavaScript 配列と繰り返し

配列と繰り返し処理

配列と繰り返し処理の1例を紹介していきます。
(例) for文を使って、配列の要素を順番に取り出す

sample.js
const color = ["red", "blue", "yellow"];

for(let i = 0; i < color.length; i++) {
    //iが配列内の要素の数だけループする
  console.log(color[i];//変数iを用いて要素を取得
}
red
blue
yellow

for文を使用することで配列の中に格納されているすべての値を簡単に出力することができます。
また、配列.lengthとすることで配列の要素数を取得できます。

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

TypeScriptで学ぶデザインパターン〜Command編〜

対象読者

  • デザインパターンを学習あるいは復習したい方
  • TypeScriptが既に読めるあるいは気合いで読める方
    • いずれかのオブジェクト指向言語を知っている方は気合いで読めると思います
  • UMLが既に読めるあるいは気合いで読める方

環境

  • OS: macOS Mojave
  • Node.js: v12.7.0
  • npm: 6.14.3
  • TypeScript: Version 3.8.3

本シリーズ記事一覧(随時更新)

Commandパターンとは

命令(あるメソッドを呼び出すといったお仕事)をクラスとして表現するためのパターンです。

サンプルコード

Commandパターンで作られたクラス群がどんなものになるのか確認していきましょう。

今回は、題材として"ある数値を2乗する簡単な機能"を想定します。GitHubにも公開しています。

modules/Command.ts

命令を表現するインターフェースです。

Command.ts
import Receiver from "./Receiver";

export default interface Command {
  setReceiver(receiver: Receiver): void;
  execute(): void;
}

setReceiverでは、命令の受け取り手を設定します。
executeでは、実際に命令を行います。命令の詳細は実装クラスのところで解説します。

modules/ConcreteCommand.ts

命令を表現するクラスです。

ConcreteCommand.ts
import Command from "./Command";
import Receiver from "./Receiver";

export default class ConcreteCommand implements Command {
  private number: number;
  private receiver: Receiver;

  constructor(number: number) {
    this.number = number;
  }

  setReceiver(receriver: Receiver): void {
    this.receiver = receriver;
  }

  execute(): void {
    console.log(this.receiver.action(this.number));
  }
}

setReceiverでは、命令の受け取り手を設定します。
executeでは、実際に命令を行います。命令の受け取り手の処理を呼び出します。

modules/Receiver.ts

命令の受け取り手を表現するクラスです。

Receiver.ts
export default class Receiver {
  action(number: number): number {
    return number * number;
  }
}

actionでは累乗した数値を返却します。

modules/Invoker.ts

命令の実行を管理するためのクラスです。

Invoker.ts
import Command from "./Command";

export default class Invoker {
  private commands: Command[] = [];

  addCommand(command: Command): void {
    this.commands.push(command);
  }

  execute(): void {
    for (const command of this.commands) {
      command.execute();
    }
  }
}

addCommandsでは、管理する対象のコマンドを追加します。コマンドを複数追加できる仕組みのおかげで過去のコマンドを呼び出したりコマンドを実行し直すことができます。
executeでは、コマンド群を順番に実行します。

Main.ts

本パターンで作成したクラス群を実行する処理です。

Main.ts
import Receiver from "./modules/Receiver";
import Invoker from "./modules/Invoker";
import Command from "./modules/Command";
import ConcreteCommand from "./modules/ConcreteCommand";

const receiver: Receiver = new Receiver;
const invoker: Invoker = new Invoker;

const threeCommand: Command = new ConcreteCommand(3);
threeCommand.setReceiver(receiver);
const fiveCommand: Command = new ConcreteCommand(5);
fiveCommand.setReceiver(receiver);

invoker.addCommand(threeCommand);
invoker.addCommand(fiveCommand);

invoker.execute();

クラス図

ここまでCommandパターンで作られたクラス群を1つずつ確認してきました。次にクラス図を示します。Commandパターンの全体像を整理するのにお役立てください。

Command.png

  • Command: サンプルコードではCommandインターフェースが対応
  • ConcreteCommand: サンプルコードではConcreteCommandクラスが対応
  • Receiver: サンプルコードではReceiverクラスが対応
  • Invoker: サンプルコードではInvokerクラスが対応
  • Client: サンプルコードではMainが対応

LucidChartを使用して作成

解説

最後に、このデザインパターンの存在意義を考えます。

命令をクラスとして持たせると履歴を管理したり過去のコマンドを再度実行したりといった管理を行うことができます。こういった命令自体を管理したいときに便利なデザインパターンとなります。

補足

サンプルコードの実行方法はこちらと同様です。

参考

あとがたり

なかなか使い所が難しい。

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

【BootstrapVueコピペのみ】導入から画像一覧画面の実装まで

スクリーンショット 2020-05-28 20.39.23.png

Vueバージョン確認

npm list vue

まずは上記コマンドでバージョンの確認

twinzlabo@0.1.0 /Users/twinzlabo

── vue@2.6.11

BootstrapVueの導入

BootstrapVueの導入がまだの方のために念のため導入方法書いときますね

とりあえずコピペして環境を整えてください

main.js
import BootstrapVue from 'bootstrap-vue'
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'

Vue.use(BootstrapVue)
npm install vue bootstrap-vue bootstrap

以上でBootstrapVueの導入は完了です

画像一覧画面の実装

説明は抜きにしてコードを下に貼ってあるのでどんどんコピペして

自分のプロジェクトに合った修正を加えてみてください

一応先に完成イメージです(モバイルに合わせてコーディングしてます)

<template>
  <b-container>
    <b-row>
      <b-col>
        <div v-for="(item, i) in items" class="images" :key="i">
          <b-img thumbnail fluid :src="item.imageUrl"></b-img>
        </div>
      </b-col>
    </b-row>
  </b-container>
</template>
<script>
export default {
  data () {
    return {
      items: [
        { imageUrl: require('@/assets/images/1.png') },
        { imageUrl: require('@/assets/images/2.png') },
        { imageUrl: require('@/assets/images/3.png') },
        { imageUrl: require('@/assets/images/4.png') },
        { imageUrl: require('@/assets/images/5.png') },
        { imageUrl: require('@/assets/images/6.png') },
        { imageUrl: require('@/assets/images/7.png') },
        { imageUrl: require('@/assets/images/8.png') },
        { imageUrl: require('@/assets/images/9.png') },
        { imageUrl: require('@/assets/images/10.png') }
      ]
    }
  }
}
</script>

いかがでしたでしょうか?
見た目はこれからですが画像一覧画面にはなったかと思います

以上です

こちらの記事にてstyleのコードまで詳しく参照できます
【Vue/BootstrapVueコピペのみ】Bootstarap導入からシンプルな画像一覧画面の実装方法までを徹底解説

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

[Javascript]繰り返し処理(while, for)

while

while (条件式) {
処理
}

let number = 1;

while (number < 10) {
  console.log(number);
  number += 1;
}

for

for (変数定義;条件式;変数の更新) {
処理
}

for (let number = 1; number < 10; number += 1) {
  console.log(number);
}

if文との組み合わせ

for (let number = 1; number < 10; number += 1) {
  if (number % 2 === 0) {
    console.log("2の倍数です");
  } else {
    console.log(number);
  }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptを用いて非同期通信実装②

とうとうajaxを使うよ

根幹であるajaxを使います。
そもそもどんなタイプがあるのかみていきます。
type:HTTPの通信方法を書く。GETかPOSTの2種類しかありません。
url:リクエストを送信する先のURLを記述する。
data:サーバーに送信する値を記述する
dataType:データ送信される型を指定する

$.ajax({
      type: 'POST',
      url: '',
      data: ,
      dataType: 'json'
    })

データタイプは今回非同期通信なのでjsonを指定しています。
これが非同期通信で忘れてはならない手順です。
次はdoneとfailについて書きます。

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

ロシア農民の掛け算をJavaScriptでやってみた

ロシア農民の掛け算

いわゆる「ロシア農民の掛け算」をJavaScriptで実装してみました。

動作原理は、ネット上にもいろいろ詳しい解説がありますが、下記のような感じです。

  1. 例題:34227 * 36070

  2. 片方を2倍、片方を半分にする。
      34227 * 36070
    = (34227 * 2) * (36070 / 2)
    = 68454 * 18035

  3. 2.を繰り返していくのが基本だが、半分にする方の数が奇数だった場合はそのままでは半分にならないので、以下のように変形する。
      68454 * 18035
    = 68454 * (1 + 18034)
    = 68454 + 68454 * 18034
    = 68454 + ((68454 * 2) * (18034 / 2))
    = 68454 + 136908 * 9017

  4. 136908 * 9017 の部分を3.と同様の手順で変形する。
      136908 * 9017
    = 136908 + 136908 * 9016
    = 136908 + 273816 * 4508

  5. 今度は 273816 * 4508 を2.と同様の手順で変形する。
      273816 * 4508
    = (273816 * 2) * (4508 / 2)
    = 547632 * 2254

  6. 半分にする方の数が 1 になるまで、2. 3. を繰り返す。

  7. 変形後の式は足し算のみになっているので、足し算を実行する。
    68454 + 136908 + 1095264 + 2190528 + 4381056 + 35048448 + 70096896 + 1121550336
    = 1234567890

ロシア農民の掛け算.png

ソース

RussianPeasant.html
<html>
<head>
  <meta http-equiv="Content-Type" context="text/html; charset=UTF-8" />
  <meta charset="UTF-8" />
  <title>ロシア農民の掛け算。</title>
<script>

function doCalc() {
    var txtN1 = document.getElementById("txtN1");
    var txtN2 = document.getElementById("txtN2");
    var n1, n2, strResult;
    var vWork = [];
    var vOut = [];

    n1 = parseInt(txtN1.value, 10);
    n2 = parseInt(txtN2.value, 10);

    vOut.push("  " + n1 + " * " + n2);

    while (1) {
        if (n2 <= 1) { break; }

        if (n2 % 2 == 0) {
            n2 = n2 / 2;
        } else {
            strResult = "= ";
            for (let i = 0; i < vWork.length; i++) {
                strResult += vWork[i] + " + ";
            }
            vOut.push(strResult + n1 + " + " + n1 + " * " + (n2 - 1));
            n2 = (n2 - 1) / 2;
            vWork.push(n1);
        }

        n1 = (n1 * 2);

        strResult = "= ";
        for (let i = 0; i < vWork.length; i++) {
            strResult += vWork[i] + " + ";
        }

        if (n2 == 1) {
            vOut.push(strResult + n1 + " * " + n2);
            vOut.push(strResult + n1);
        } else {
            vOut.push(strResult + n1 + " * " + n2);
        }
    }

    var nResult = 0;
    for (let i = 0; i < vWork.length; i++) {
        nResult += vWork[i];
    }
    nResult += n1;
    vOut.push("= " + nResult);

    document.getElementById("result").innerHTML = vOut.join("\n");
}

</script>
</head>
<body>

<h2>ロシア農民の掛け算。</h2>
5桁 * 5桁 まで<br />
<input type="text" id="txtN1" value="34227" maxlength="5" size="7" oninput="value = value.replace(/[^0-9]+/i,'');" /> ×
<input type="text" id="txtN2" value="36070" maxlength="5" size="7" oninput="value = value.replace(/[^0-9]+/i,'');" />
<input type="button" value="計算" onclick="doCalc()" /><br />
<br />

<textarea id="result" rows="50" col="20" style="width:700px;height:400px;">
</textarea>

</body>
</html>

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

【Vue/Font Awesome導入時のエラー解決】error There should be no space after this paren space-in-parens

スクリーンショット 2020-05-28 17.03.05.png

Vueバージョン確認

npm list vue

まずは上記コマンドでバージョンの確認

twinzlabo@0.1.0 /Users/twinzlabo

── vue@2.6.11

エラーメッセージ

Failed to compile.

./src/main.js Module Error (from ./node_modules/eslint-loader/index.js): /Users/twinzlabo/src/main.js

17:15 error Multiple spaces found before ”font-awesome-icon” no-multi-spaces
17:15 error There should be no space after this paren space-in-parens
17:53 error Multiple spaces found before ‘)’ no-multi-spaces
17:53 error There should be no space before this paren space-in-parens

解決策

今回のエラー文で注目すべき箇所は

17:15 error Multiple spaces found before ”font-awesome-icon” no-multi-spaces
17:15 error There should be no space after this paren space-in-parens
17:53 error Multiple spaces found before ‘)’ no-multi-spaces
17:53 error There should be no space before this paren space-in-parens

つまりmain.js17行目でスペースがおかしい使い方されているよというエラーです

ではまず実際のエラー箇所を確認してみましょう

main.js
// about fontawesome---
import { library } from '@fortawesome/fontawesome-svg-core'
import { faCoffee, faSpinner, faAngleDoubleUp } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
// -------------------
import App from './App.vue'
import router from './router'
import store from './store'

library.add(faCoffee, faSpinner, faAngleDoubleUp)
Vue.component(  'font-awesome-icon', FontAwesomeIcon  )

どうやらFont Awesome導入時にエラーが発生してしまったようですね

Vue.component( ‘font-awesome-icon’, FontAwesomeIcon )

こちらぱっと見は全然問題なさそうですが、

‘font-awesome-icon’, FontAwesomeIcon

この前後にスペースが1つずつ存在してしまっています

細かいですがESlintではこの程度でもエラーを表示してしまうので気をつけましょう

修正後のコードはこのようになります

// about fontawesome---
import { library } from '@fortawesome/fontawesome-svg-core'
import { faCoffee, faSpinner, faAngleDoubleUp } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
// -------------------
import App from './App.vue'
import router from './router'
import store from './store'

library.add(faCoffee, faSpinner, faAngleDoubleUp)
Vue.component('font-awesome-icon', FontAwesomeIcon)

これで無事解決しましたね

以上です

参考記事
【Vue/Font Awesome導入】Font Awesomeを導入してアイコンを使用するまでの流れを徹底解説

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

【jQuery】ぺージのスクロールボタンを作りたい

ページの一番上や、特定の位置にスクロールさせるボタンを設置する際の
簡潔な手順をご紹介。

1. htmlでボタンを準備

今回はフッターの中に設置しました。

sample.html
<div class="footer">
  <div class="footer__btn">
    Page Top
  </div>
</div>

2. 位置を固定する

スクロール中に隠れないように、「z-index」も調整しておきます。

sample.css
.footer__btn {
  cursor: pointer;
  height: 80px;
  width: 80px;
  z-index: 10;
  position: fixed;
  bottom: 20px;
  right: 32px;
  background-color: red;
  line-height: 80px;
  text-align: center;
  font-size: 20px;
}

3. jsを準備する

ボタンのクリックイベントによって、ページの最上部までスクロールさせます。

sample.js
var scrollTop = window.pageYOffset ;     //スクロール量を取得
  $(".footer__btn").on("click", function(){   //クリックした時に発火
    $( 'html,body' ).animate( {scrollTop:0}, 'slow' );  //ゆっくり一番上までスクロールさせる
  })
  $(".gy-btn").on('click', function(){
    $( 'html,body' ).animate( {scrollTop:1500}, 'slow' );
  })

ヘッダーのボタン等で指定位置までスクロールさせたい場合は
scrollTopの値を変更します。

sample.js
var scrollTop = window.pageYOffset ;     
  $(".other-btn").on('click', function(){
    $( 'html,body' ).animate( {scrollTop:1500}, 'slow' );
  })

以上で終了です。
ご覧いただきありがとうございました。

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

webpack 5 Module Federationにおけるchunk最適化

(※これからかくことは次回リリースで変更される可能性もあります)

Module Federation概要

webpack 5でModule Federationという仕組みが入る。

例えば、以下のように書くと、app_01shredオプションに記述したモジュールが、app_02のアプリに対して共有されるようになる。

plugins: [
  new ModuleFederationPlugin({
    name: "app_01",
    library: { type: "var", name: "app_01" },
    filename: "remoteEntry.js",
    remotes: {
      app_02: "app_02",
    },
    exposes: {
      SideNav: "./src/SideNav",
      Page: "./src/Page"
    },
    shared: ["react", "react-dom", "@material-ui/core", "react-router-dom"]
  }),
]
plugins: [
  new ModuleFederationPlugin({
    name: "app_02",
    library: { type: "var", name: "app_02" },
    filename: "remoteEntry.js",
    remotes: {
      app_03: "app_03",
    },
    exposes: {
      Dialog: "./src/Dialog",
      Tabs: "./src/Tabs"
    },
    shared: ["react", "react-dom", "@material-ui/core", "react-router-dom"]
  }),
]
<!DOCTYPE html>
<html lang="en">
  <head>
    <!-- app_02 -->
    <script src="http://localhost:3002/remoteEntry.js"></script>
  </head>
  <body>
    <div id="root"></div>
    <!-- app_01 -->
    <script src="http://localhost:3001/main.js"></script>
    <script src="http://localhost:3001/remoteEntry.js"></script>
  </body>
</html>

app_02のモジュールは非同期に読み込まれ、実行時にchunkを解決する。実行するまで不明なプログラムであるはずなので、エラーなどもその時までわからない。

しかし、逆に言えば、app_01app_02のビルドパイプラインは疎結合にできるので、Micro-Frontendsにおいて活用できそう、というわけだ。

overridablesとoverrides

ModuleFederationPluginでは、ContainerPluginContainerReferencePluginというクラスを内部で扱っている。詳細はこちらのとおりだが、それぞれがoverridablesoverridesというオブジェクトを提供している。

  • overridables - remoteとして読み込まれたとき、localによって上書き可能なモジュール
  • overrides - localとして読み込まれたとき、remoteを上書きするモジュール

これをハッシュで識別し、実行時に解決・最適化するのがModule Federationの実態だ。前述の例では、sharedオプションに指定されたモジュールが上書き対象となっている。

sharedの問題

しかし、sharedオプションをつかったchunkにはひとつ問題がある。

ModuleFederationPluginlocalremoteそれぞれのコードを生成する箇所は以下のようになっている。

compiler.hooks.afterPlugins.tap("ModuleFederationPlugin", () => {
  if (
    options.exposes &&
    (Array.isArray(options.exposes)
      ? options.exposes.length > 0
      : Object.keys(options.exposes).length > 0)
  ) {
    // remote 用のコード
    new ContainerPlugin({
      name: options.name,
      library: options.library || compiler.options.output.library,
      filename: options.filename,
      exposes: options.exposes,
      // options.shared その1
      overridables: merge(options.shared, options.overridables)
    }).apply(compiler);
  }
  if (
    options.remotes &&
    (Array.isArray(options.remotes)
      ? options.remotes.length > 0
      : Object.keys(options.remotes).length > 0)
  ) {
    // local 用のコード
    new ContainerReferencePlugin({
      remoteType:
        options.remoteType ||
        (options.library && options.library.type) ||
        compiler.options.externalsType,
      remotes: options.remotes,
      // options.shared その2
      overrides: merge(options.shared, options.overrides)
    }).apply(compiler);
  }
});

これをみると、options.sharedは2箇所でつかわれていることがわかる。つまり、sharedを使うと、overridesoverridablesを同時に提供してしまうので、アプリのユースケースよっては不要なコードが生成されてしまうのだ。

具体的なパターンをみてゆく。

overridablesのみ提供したいパターン

例えば、app_02を以下のように仮定する。

  • app_02remoteとして参照されることがあり、いくつかのライブラリをlocal(app_01)に上書きしてほしい
  • app_02からremote(app_03)を呼ぶケースでは、app_02remoteで共通するライブラリがない
  • この場合「app_02overridablesのみを提供し、overridesを提供することはない」ということになる。

shredを使うと、local用のビルドでoverrideschunkもつくってしまう。しかし、前述のような想定だと、overrideschunkが使用されるケースはない。

これを解決するのがoverridablesオプションである。sharedの代わりにoverridablesと明示的に指定することで、overridesが生成されることはなくなる。

plugins: [
  new ModuleFederationPlugin({
    name: "app_02",
    library: { type: "var", name: "app_02" },
    filename: "remoteEntry.js",
    remotes: {
      app_03: "app_03",
    },
    exposes: {
      Dialog: "./src/Dialog",
      Tabs: "./src/Tabs"
    },
    // 変更箇所
    overridables: ["react", "react-dom", "@material-ui/core", "react-router-dom"]
  }),
]

実際には、以下のようにchunkを削除できる。

スクリーンショット 2020-05-28 18.16.18.png

overridesのみ提供したいパターン

同様に、例えば、app_01を以下のように仮定してみる。

  • app_01app_02remoteとして参照し、いくつかのライブラリを上書きしたい
  • app_01はライブラリのバージョンが古いため、remoteとして参照される場合に、ライブラリを上書きされたくない
  • この場合「app_01overridesのみを提供し、overridablesを提供することはない」ということになる。

shredを使うと、remote用のビルドでoverridableschunkをつくってしまう。前述のような想定だと、overridableschunkが使用されることがないし、また、生成されるべきでもない。

これはoverridesオプションで解決できる。sharedの代わりにoverridesを使うことで、overridablesが生成されることはなくなる。

plugins: [
  new ModuleFederationPlugin({
    name: "app_01",
    library: { type: "var", name: "app_01" },
    filename: "remoteEntry.js",
    remotes: {
      app_02: "app_02",
    },
    exposes: {
      SideNav: "./src/SideNav",
      Page: "./src/Page"
    },
    // 変更箇所
    overrides: ["react", "react-dom", "@material-ui/core", "react-router-dom"]
  }),
]

実際には、以下のようにchunkを削除できる。

スクリーンショット 2020-05-28 18.18.23.png

雑感

  • 実際はこれらを手で管理してゆくのは厳しくないか・・?
  • アプリケーションのユースケースと責務がはっきりしていれば、明示的に指定することはできるかもしれない
  • 最初から意識しておけばsharedを使うことこと自体は減りそう
  • automatic-vendor-federationというプラグインがあるが、現状はsharedしか対応していない
  • chunkが生成されてしまうだけで、fallbackもあるのでバグるわけではない。あくまで最適化したい場合
  • 脳死sharedでもそんなには困らないのかも

参考

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

console.logでフォーマット指定子を使う!

console.log()

JavaScriptのconsole.log()でC言語のようなフォーマット指定子が使えることを知ったので、まとめておきます。

通常

とりあえず通常の書き方
console.log("hoge")
image.png

%s

ただの文字列
いまいち使い道が分かりません
console.log("%s", hoge)
image.png

こっちで十分ですね。
image.png

しいて言うならこれですかね。
image.png
...arryはスプレッド構文という書き方で、配列やオブジェクトを展開してくれます。

%oまたは%O

つまずきがちなObjectの出力
consol.log("%o", hoge)
image.png

これでもできますけどね
image.png

ちなみに%O
いまいち違いが分かりません
image.png

%dまたは%i

お次は整数値です。
console.log("%d", hoge)
image.png

0埋めや半角スペース埋めもできます。
これはChromeやSafariでは使用できませんでした。
下記画像はFirefoxでの検証結果です。
image.png

ちなみに小数を表示しようとすると、小数点以下は切り捨てられます。
入力: 1.1 → 出力: 1
入力: -1.1 → 出力: -2

%f

浮動小数点です。
console.log("%f", hoge)
image.png

小数点以下の桁数指定ができます。
これもChromeやSafariでは使用できなかったので、Firefoxでの検証結果です。
image.png

%c

出力にCSSスタイルを適用できます。
console.log("%cあいうえお", style)
image.png


参考サイト

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

【神業】面倒なHTMLタグ入力を光の速さで終わらせる【定型文ツール】

はじめに

フリーランスRailsエンジニアおよびWEBライターをやっているソエノと申します。この記事では「面倒な定型文入力を神速で終わらせる方法」についてご紹介します。

注意事項 : コメント欄への書き込みについて

コメント欄への「誹謗中傷・暴言」の書き込みはインターネット上であれ「威力業務妨害」が成立する危険性があります。全てのコメントは削除後も筆者のメールアドレスに保存されていますので、何卒ご注意の程よろしくお願いいたします。

結論

「Dash」というツールを使います。以下のようなことができます。以下は、HTMLのaタグを書くときの例です。

a241ccbb5154265091554405f3d18873.gif

う〜ん、神。普通、aタグを書こうとしたら<a href="hoge">fuga</a>のhogeとfuga以外の部分って手入力しますよね。上記の動画では;atagという呪文によって謎のウィンドウを召喚し、hogeとfugaだけを入力してEnterすることによって何とaタグを完成させてしまいました。変換キーとかも使ってないので、もはや辞書登録使うより速いです。

このツールを使うなら、ブロガーやフロントエンジニアの方はHTMLタグを書くために<a href="hoge">fuga</a>みたいなテキストを手入力するのは金輪際やめましょう。Railsエンジニアなら「routes.rbのresourcesってどういう風に書くんだっけ...」とか「バリデーションってどう書くんだっけな?ググるか...」みたいなことが今後一切なくなります。

ダウンロード方法

こちらからダウンロードできます。リンク先にアクセスしたら、画面右にあるDownloadをクリックです。zipファイルがダウンロードされるので、解凍してアプリケーションフォルダにアプリを移動させましょう。

使い方

使う前に、アプリの動作許可設定しないと動かないのでMacのシステム環境設定を開きましょう。パソコン左上に表示されているりんごマークをクリックしたら「システム環境設定」というのがありますから選びます。スクリーンショット 2020-05-28 14.57.32.png

スクリーンショット 2020-05-28 14.58.04.png

家のマークで「セキュリティとプライバシー」というのがありますので、これを選びます。下記画面で左側のウィンドウを少し下にスクロールすると、「アクセシビリティ」という項目があるので、選びます。そして左下の錠マークをクリックしてパソコンのロックを解除します。あとは「Dash」というところのチェックをONにしましょう。これで準備OKです。

スクリーンショット 2020-05-28 14.59.20.png

それではDashを起動しましょう。

スクリーンショット 2020-05-28 15.03.11.png

起動後の画面はこんな感じです。まずは右上の「Manage Docsets」をクリックしましょう。

スクリーンショット 2020-05-28 15.03.52.png

上記のような画面になります。続けて、上のメモマーク「Snippets」を選択しましょう。

スクリーンショット 2020-05-28 15.07.02.png

Snippetsを選択したら上記のような画面になるので、Enable snippetsというチェックをONにしましょう。ONにしたらこの設定画面は不要なので左上の×マークで閉じます。

スクリーンショット 2020-05-28 15.14.44.png

設定後、ホーム画面のSearchという欄でSnippetsというのが選択できるようになりますので、クリックします。

スクリーンショット 2020-05-28 15.15.49.png

すると上記のようになるので、New Snippetsをクリック。

スクリーンショット 2020-05-28 15.16.22.png

なんか入力欄が二つ出てきます。上の細い入力欄が、先ほど紹介した「呼び出しの呪文」です。下の大きなの欄が「実際に呼び出されるテキスト」になります。試しに、次のように設定してみましょう。上の入力欄には;atag、下の入力欄には<a href="__link__">__text__</a>と書きました。

スクリーンショット 2020-05-28 15.18.04.png

設定はこれで完了です。特に保存とかは無いみたいなので、このままDashアプリからは離れてしまって構いません。あとは、適当なテキスト入力欄に;atagと打ってみてください。なんか召喚されると思います。

新しいスニペットを登録する

この「スニペット」というのが先ほどのように「召喚の呪文と、そこから召喚されるテキストの組み合わせ」という意味のようです。新しいスニペットを登録するには、下記動画のように画面上のプラスマークをクリックすればいいようです。

20c74c6c0dffda9faa872fab231e6f92.gif

さらに高速化していく方法【カスタマイズ機能】

上記では__link____text__といった記述によってテキストを入力できるようにカスタマイズしました。これは「アンダースコア2つで囲むと任意テキスト入力できるようにする」というDashのカスタマイズ機能です。このほかにも便利な機能がいくつもありますので、使えそうなものをピックアップしてご紹介します。

クリップボードの情報を自動で挿入する

ここでいうクリップボードというのは「コピーしたテキスト」のことです。例えば、さっきのaタグ生成のやつを以下のように書くと、直前にURLをコピーしていれば、コピーしたテキストを勝手にペーストしてくれます。

スクリーンショット 2020-05-28 15.32.53.png

93c5b8717f74c67fc7893ef62a7a3925.gif

@clipboardという記述がそのままクリップボードの情報に置き換わるようです。

挿入後のカーソルの位置を指定する

さっきはDashで呼び出されたウィンドウ上で文字入力していましたが、先にaタグを生成してからエディタ上でテキストを入れたい時はこれが便利です。@cursorと書くと、スニペット挿入後に勝手にカーソルが@cursorと書いた位置に飛んでくれます。

スクリーンショット 2020-05-28 15.43.18.png

de3a1be8afb5d53bfd35edfe550621d5.gif

便利なプリセット

この記事をここまで呼んでくださった方のために、プリセットをご用意してみました。よろしければ、ご活用ください。

HTML

aタグ
;atag
<a href="@clipboard">@cursor</a>
divタグ
;divtag
<div class="__class__">@cursor</div>
hタグ(h1,h2,h3,...)
;htag
<h__number__>@cursor</h__number__>
pタグ
;ptag
<p>@cursor</p>
scriptタグ(JavaScript埋め込み)
<script type="text/javascript">
  @cursor
</script>
任意のHTMLタグ
;html
<__html__>@cursor</__html__>

Ruby on Rails

routes.rbのresources
;resources
resources :__controllerName__, only: [:__action1__, :__action2__]
form_with(haml記法)
;form_with
= form_with model: __modelName__, url: __actionUrl__, local: __trueOrFalse__ do |f|
  @cursor
バリデーション(存在)
;validate
validates :__columnName__, presence: true

JavaScript

console.log
;console
console.log("@cursor");
jQueryを使ったDOM取得
;jq
$("@cursor")

記事を読んでいただきありがとうございました

ここまで読んでくださって本当にありがとうござます。私から皆さんにGiveさせて頂けるのはこうしたエンジニア向け情報くらいしかありませんが、皆さんに記事を読んでいただけて、そして皆さんのお役に立つことができれば、それは私の何よりの喜びになります。今後とも皆さんのお役に立てるよう記事執筆活動などを進めていきたいと思っていますので、これからも何卒よろしくお願いいたします。

まとめ

・Dashというアプリを使うことで、HTMLタグの入力を楽にしたり、Railsなどのプログラミング言語における「定型句」を覚える必要がなくなりました。代わりに自分で登録した「召喚の呪文」を覚える必要がありますが、これまでの作業よりも幾分マシになっているはずです。

以上となります。それでは、生産的なコーディングライフを!!

筆者Twitter: soeno_onseo

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

function式とarrow式の違いをサッカー選手を呼び出しながら説明するよ

概要

function式とarrow式の違いをサッカー選手を使いながら軽く説明するよ

問題

早速、問題を出します。
aとbの違いわかりますか?

const a = function() {
  console.log('neymar')
}

const b = () => {
  console.log('neymar')
}

この場合だとbのほうが合計文字数がすくなくなったくらいですかね。

// let, const であればトップレベルで宣言してもwindowオブジェクトのプロパティを生成しませんが今回は問題の都合上varでいきます
// よほどのことがない限りこんな書き方をしないけどグローバルのnameに代入するために記載
// var宣言が怖い理由の一つもこれですね。

var name = '??global-messhi??'

const printName = function() {
  console.log('printName', this.name)
}

const arrowPrintName = () => {
  console.log('arrowPrintName', this.name)
}

const obj1 = {
  name: 'nagatomo',
  printName
}

const obj2 = {
  name: 'king-kazu',
  arrowPrintName
}

// 出力される名前はなんでしょう
obj1.printName()
obj2.arrowPrintName()


答え
obj1.printName() // nagatomo
obj2.arrowPrintName() // ??global-messhi??


アロー関数式とfunction式の違い

先程の問題の結果を見て、文字数が短くなっただけではないことはすでにわかったと思います。

thisの指している場所が違います。

通常のfunction式の中でthisを使うと、その呼び出し元のオブジェクトを指します。

var name = '??global-messhi??'

const printName = function() {
  console.log('printName', this.name)
}

const obj1 = {
  name: 'nagatomo',
  printName
}

const obj2 = {
  name: 'king-kazu',
  printName
}

obj1.printName() // 'nagatomo'
obj2.printName() // 'king-kazu'

なのでこの場合printName関数内で使っているthis.nameの値は呼び出し元がobj1とobj2なのかで値が異なってきます。
function式だとthisが呼ばれたタイミングで決定されちゃんと自立できていない人のような振る舞いです。
なのでfunction式は直感でわかりにくいので苦労します。

アロー関数は結論から言うとthisは宣言された時点で、thisを確定します。

var name = '??global-messhi??'

const arrowPrintName = () => {
  console.log('arrowPrintName', this.name)  // この時点でのthisはグローバルになる
}

const obj1 = {
  name: 'nagatomo',
  arrowPrintName
}

const obj2 = {
  name: 'king-kazu',
  arrowPrintName
}

obj1.arrowPrintName() // '??global-messhi??'
obj2.arrowPrintName() // '??global-messhi??'

なので、呼び出し元がobj1だろうが、obj2だろうが、関係なくて直感的にthisの指している場所がわかります。

今学んだことを再確認するために、もっかい問題出します。

class Person {
  constructor(name) {
    this.name = name
  }
  printName = function () {console.log('printName', this.name)}
  arrowPrintName = () => {console.log('arrowPrintName', this.name)}
}

const printName = function() {
  console.log('printName', this.name)
}

const arrowPrintName = () => {
  console.log('arrowPrintName', this.name)
}

class Person2 {
  constructor(name) {
      this.name = name
  }
  printName = printName
  arrowPrintName = arrowPrintName
}


const kakitani = new Person('kakitani')
const maezono = new Person2('maezono')

// 出力される名前はなんでしょう
kakitani.printName() 
kakitani.arrowPrintName()
maezono.printName()
maezono.arrowPrintName()


答え
kakitani.printName()  // kakitani
kakitani.arrowPrintName() // kakitani
maezono.printName() // maezono
maezono.arrowPrintName() // ??global-messhi??


const bindPrintName = function() {
  console.log(this.name)
}.bind(this) // function式をarrow関数と同じ感じでthisをつかえるようにするための魔法のコトバだよ

class Person3 {
  constructor(name) {
    this.name = name
  }
  bindPrintName = bindPrintName
}

// 出力される名前はなんでしょう
const zaccheroni = new Person3('zaccheroni')
zaccheroni.bindPrintName()


答え
const zaccheroni = new Person3('zaccheroni')
zaccheroni.bindPrintName() // ??global-messhi??


Reactでよくあるパターン

これだとボタンを押した時Uncaught TypeError: Cannot read property 'state' of undefinedが出ます。
理由わかりますか?

export default class Hoge extends Component {
  constructor(props) {
    super(props);
    this.state = {
      name: 'zico'
    };
  }

  handlePrintName() {
    const { name } = this.state;
    return name
  }

  render() {
    const { name } = this.state;

    return (
      <button onClick={this.handlePrintName}>
        print name
      </button>
    );
  }
}

解決方法

handlePrintName()をアロー関数にする

  // 変更前
  handlePrintName() { // これでthisが定まっていない
    const { name } = this.state;  
    return name
  }
  // 変更後
  handlePrintName = () => { // これでthisをHogeにbind
    const { name } = this.state;  
    return name
  }

ただReactの場合これには問題点があります。

レンダー内でアロー関数を利用するとコンポーネントがレンダーされるたびに新しい関数が作成されるため、子コンポーネントでReact.memoやPureComponentを使ってた場合に正しく比較されなくなる

ClassComponentではbind(this)を使う場合が多い

export default class Hoge extends Component {
  constructor(props) {
    super(props);
    this.state = {
      name: 'zico'
    };
  this.handlePrintName  = this.handlePrintName.bind(this)
  }

  handlePrintName() {
    const { name } = this.state;
    return name
  }

  render() {
    const { name } = this.state;

    return (
      <button onClick={this.handlePrintName}>
        print name
      </button>
    );
  }
}

参考記事

JavaScriptの「this」は「4種類」??
JavaScript の this を理解する多分一番分かりやすい説明

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

kintone ポータルをアプリで動的に制御

github repositoryはこちらから。 見た目 仕様 ポータルの表示内容を動的にアプリのレコードで制御をすることができます。 初心者の方も簡単に利用できるようになってます。 注意: この自動ポータル機能の設定には管理者権限で、 kintone全体カスタマイズにjsファイルをアップロードする必要があります。 REST APIを使い、アプリからポータルへ、レコード取得をしているので ユーザーが多い環境では1日1万件のリクエスト上限を超え、 ポータルカスタマイズが利用できなくなる可能性もあります。 導入の流れ アプリテンプレートからアプリ作成 レコード作成 JSの一部を変更 jQueryと一緒に全体カスタマイズ保存 githubから関連ファイルをダウンロード githubにアクセスして、 右上の↓CodeからDownload Zipでファイルをダウンロード。 ダウンロードできたら、zipファイルを展開します。 (エクスプローラー上で右クリック→すべて展開) Portal_Setting アプリ作成 アプリは全員が閲覧権限を持つスペース、 また、そのようなスペースが存在しない場合は、 ポータルトップ上でアプリの作成をして下さい。 先ほど、展開したファイルの中にtemplatesというファイルがあります。 その中のportal_manage.zipをアプリ作成の、 テンプレートファイルを読み込んで作成に使用します。 設定 アプリ名は変えていただいて問題ありません。 アプリの設定はテンプレートから作成のままで変更は特にありません。 入力制御も最低限、プログラムで補っています。 フィールドコード、フィールド名はプログラムに使用している箇所もあります。 不必要に変えないようにしてください。 一覧はデフォルトで見やすいように設定しているだけなので、 絞り込みなどを参考に追加、作り直し問題ありません。 閲覧権限 ログインユーザーによってポータルの表示内容変更ができます。 アプリショートカット、スペースの表示は、 kintone従来のレコードの閲覧権限で制御できます。 レコードの閲覧権限についてはこちらから。 レコード登録 ポータル背景 お知らせ アプリショートカット スペース 上記の4項目を設定します。 ポータル背景 1度データ作成した後に変更する場合はデータを編集してください。 フィールド 内容 ポータル背景_画像 ポータルの背景になる画像ファイル アプリ群タイトル アプリショートカット枠のタイトル お知らせタイトル お知らせ枠のタイトル スペースタイトル スペース枠のタイトル お知らせ一覧 お知らせ内容はリッチエディタの内容がHTML形式でトップに表示されます。 優先順位は更新日時が一番最新のものが1件表示されるようになっています。 お知らせの履歴を残すこともできますが、 レコード数が多くなればなるほど、ポータルの表示速度が遅くなる可能性があります。 推奨はレコード編集で随時変更する運用です。 フィールド 内容 お知らせ内容 リッチエディタ アプリショートカット 設定数制限はありませんが、ショートカットなのでスマートに使いましょう。 メイン項目 アイコンファイルは正方、(100px X 100px等)を推奨しています。 ファイルは1つまでです。 フィールド 内容 アプリ優先順位 ポータルに表示する並びの指定 アプリID アプリID、URL指定に使いますアプリトップのhttps://sample.cybozu.com/k/XXXXの数字 アプリアイコン アプリアイコン画像ファイル デザイン 少し難しいかもしれませんが、デザインの幅を広げるため、 グラデーションカラー設定を可能にしています。 詳細はこちら。 カラーの指定方法(例) #ffce44 100% またはrgb rgb(255,0,0) 100% フィールド 内容 数値 グラデーションの傾き設定 カラー1 指定方法: (カラーコード[半角スペース]濃さ%) カラー2 指定方法: (カラーコード[半角スペース]濃さ%) 例: linear-gradient(180deg,#F4CC70 0%,#DE7A22 100%) 以下のテーブル内容で上記のCSSがアプリアイコンのbackgroundに適用されます。 フィールド 内容 数値 180 カラー1 #F4CC70 0% カラー2 #DE7A22 100% スペース フィールド 内容 スペース優先順位 ポータルに表示するスペースの並びの指定 スペースID スペースID、URL指定に使いますスペーストップのURLhttps://sample.cybozu.com/k/#/space/XXXXの数字 スペース名 スペースの名前(実際のスペースの名前と違っても、問題ないです。) jsファイルの編集 ダウンロードしたファイルのtemplatesのjsフォルダ中に、 ~~~desktop.js, ~~~mobile.js というファイルがあります。 初心者の方 右クリックで編集を選択後、警告を確認して開いてください。 下記の1の数字を先ほど作成したアプリのアプリIDを半角数字で入れる (function() { "use strict"; //半角数字で作成したアプリのIDを設定 const APPID = 1; ----------------以下省略------------------ 例:アプリのページを開いた時のurlが https://xxxx.cybozu.com/k/666/ の時、 666; セミコロン;は消さないでください。 (function() { "use strict"; //半角数字で作成したアプリのIDを設定 const APPID = 666; ----------------以下省略------------------ 今回のカスタマイズで必要なもの 全体カスタマイズ: PC用のJavaScriptファイル https://js.cybozu.com/jquery/3.5.0/jquery.min.js Desktop用ファイル スマートフォン用のJavaScriptファイル https://js.cybozu.com/jquery/3.5.0/jquery.min.js Mobile用ファイル コピペURLを追加をして大丈夫です。 kintoneポータル設定 ポータル右上の設定からポータル設定ができます。 ポータルコンテンツの表示に関しては、どの設定でも問題はありません。 ポータルを整理するのであれば、通知と未処理の項目くらいがベスト。 参考 kintone help レコードの閲覧権限を設定する Kintone Portal Designerを使ってポータルをデザインしよう MDN web docs linear-gradient()
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Yellowfinのv9ダッシュボードでレポートの値からフィルターを制御する

概要

前回の記事ではレポートの値をフォームに入れてPOSTしましたが、今回は応用でレポートの値でフィルターを効かせるようにします。
filter.gif

表示するレポートを選ぶ

今回は、事前にフィルター付きのレポートを用意します。
ここでは、YellowfinのチュートリアルデータであるSkiTeamのデータにあるCamp Performanceのレポートを元にしてCamp Regionのカラムをフィルターにセットします。(演算子:一覧に含む、値:ユーザープロンプト、フィルターの書式→表示→説明でフィルターの名前をfilterRegionに変更しています)
image.png

フィルターの入力スタイル

下記のように「一覧から値を選択」と表示タイプを「一覧」にします。他の表示方法だとJavascriptでフィルターとレポートの同期ができませんでした・・・・
image.png

ダッシュボードの作成

ダッシュボードを作成し、先程のレポートをドラッグアンドドロップで配置します。
image.png
フィルターも同様に、ドラッグアンドドロップします。
image.png
+フィルターボタンを押下し、ポップアップでレポートで作成したフィルターを紐つけます。
image.png

コードモードの編集

ここからコードモードに変更し、HTMLタブを見ていきます。

HTMLタブ

大体こんな感じになっていると思います。report-outputタグのnameがレポート名になっています。

dashboard.html
<canvas-area xmlns="http://www.w3.org/1999/xhtml" canvas-uuid="eecfc9c0-fd84-4c5f-a4f4-692b6e8ab85b">
    <report-output widget-uuid="4956314f-eac8-4563-8d65-b394fbb1ae53" report-uuid="db89f236-f4fb-4f3d-852e-9ae2e43f41aa" height="300" top="0" left="353" name="for qiita, Camp Performance" width="400" display-type="TABLE" style="z-index: 1;"></report-output>
    <filter-list widget-uuid="8d08b89d-d2a4-494c-8794-e7d9131e96e6" filter-list-uuid="b15da1ed-9c9b-4e00-9e5c-3ed1fca8dcb2" width="280" height="382" top="0" left="0" hide-control-panel="false" dash-filter-auto-run="false" hide-reset-link="false" name="フィルター一覧(縦)" style="z-index: 2;"></filter-list>
</canvas-area>

JSタブ

レポート変数にhtmlのレポートのnameを指定し、フィルター名はレポート作成時に設定した表示名をthis.apis.filters.getFilterで指定します。

jstab.js
let report = this.apis.canvas.select('for qiita, Camp Performance');

/* Use APIs to apply filters */
report.addEventListener('click', (event) => {
    let $row = $(event.target).closest('tr');
    let demographicFilter = this.apis.filters.getFilter('filterRegion');
    let text = $row.find('td').get(0).innerText;
//ちなみにですが、ボタンのクリックイベントにしておいて、あらかじめtext変数を配列にしておけば、複数のフィルターの値で絞り込みができるようになります。
    //let text = ['Europe', 'North America'];

    this.apis.filters.setFilterValue(demographicFilter.get('filterId'), text);
    this.apis.filters.applyFilters();
})

こんな感じで

レポートの行をクリックした時にイベントが起動して、その行の一番左の値をフィルターに指定するダッシュボードが完成しました。これでより直感的なダッシュボードが作成できるようになるのではないでしょうか。
※一度完成したダッシュボードでも元のレポートやフィルターを変更したり更新したりすると、うまく動かなくなることがあるようです、、、この辺はアップデート待ちなんでしょうかね。。

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

Twilio Video SDKを使った時にgetUserMediaが複数回走ってしまう時の対応方法

Twilio Video SDKを使った時に、connectを呼ぶたびにgetUserMediaが走ってユーザーに許可を求めてしまったのでその対処方法をメモ。

環境

  • Twilio Video SDK v2.5

やりたかったこと

最初にnavigator.mediaDevices.getUserMedia()を実行しstreamを取得して、以後それを使いまわしたかった。

起こったこと

Twilio.video.connectを呼ぶたびに、getUserMediaが走って、ユーザーに許可を求めてしまう。

原因

Twilio.video.connectを実行した時、デフォルトだと自動でstreamを取得し、そのあとのstreamの管理も行ってくれる。

APIドキュメントはこちら

対応方法

navigator.mediaDevices.getUserMediaで取得した、streamをconnectにオプションとして渡す。

navigator.mediaDevices.getUserMedia({
  audio: true,
  video: true
}).then(function(mediaStream) {
  return Video.connect(token, {
    name: 'my-cool-room',
    tracks: mediaStream.getTracks() // tracksでstreamを渡す
  });
}).then(function(room) {
  room.on('participantConnected', function(participant) {
    console.log(participant.identity + ' has connected');
  });

  room.once('disconnected', function() {
    console.log('You left the Room:', room.name);
  });
}).catch(error => {
  console.log('Could not connect to the Room:', error.message);
});
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ビデオチャット中に左シフトキーを押している間だけミュート解除し会話できるアプリをつくった

Google MeetやZOOMでビデオチャットをしているとき、マイクに雑音が入らないよう自分の発言中以外は基本的にミュートしているのですが、
ミュート解除を切り忘れたり、別画面を開いているとすぐにミュート解除できなかったりで、面倒だったのでキーボードの特定のキーを押している間だけミュートを解除してくれるアプリ「Cough Switch」を作成しました。

トランシーバーやレコーディングスタジオのトークバック機能のようなイメージです。

output2.gif

AppleScriptでマイクの音量を操作する

Macの場合、 システム環境設定 > サウンド > 入力 でマイク設定を操作することができます。
この入力音量を0にするとミュートされます。

スクリーンショット 2020-05-28 11.51.38.png

applescript
tell application "System Events" to set volume input volume 100

このスクリプトをjsから実行するにはこんな感じ

javascript
const applescript = require('applescript');
applescript.execString('tell application "System Events" to set volume input volume 100', function);

iohookですべてのキーボードイベントを取得する

さくっと作りたいのでまたElectronで簡単にアプリ化していきます。

Webのいつも通りにキーボード操作を取得しようと、
document.addEventListener("keydown", function)
としてしまうと、Windowがアクティブになっていないとキーボードイベントを取得できません。

electron.globalShortcutではkeyupイベントを取得できないので却下。

他に探してみたところiohookってやつで取得できるみたいです。
(特定のバージョンのNodeとElectronしかサポートしていないらしくちょっとはまった)

javascript
const iohook = require('iohook');

const KEYCODE = 42; // left shift key

iohook.on('keydown', (msg) => {
  if (msg.keycode === KEYCODE) {
    talk();
  }
});

iohook.on('keyup', (msg) => {
  if (msg.keycode === KEYCODE) {
    mute();
  }
});

どのキーに割り当てるか悩みましたが2つあるので、左シフトキーにしました。

Electronの最前面の触れない透明なWindowを作成

ミュート解除中のアイコンを表示させるために、最前面に触れないWindowを作成します。
さらに邪魔にならないよう背景を透明にします。

javascript
const appWindow = new BrowserWindow({
  transparent: true,
  frame: false,
  resizable: false,
  alwaysOnTop: true,
});

appWindow.setIgnoreMouseEvents(true);

appWindow.loadURL(`file://${__dirname}/index.html`);

これで最前面にHTMLが表示されます。
あとはキーボードイベントに応じて、Windowへ状態を送り、CSSでアイコンを表示させます。

javascript
appWindow.webContents.send('talk');

おわり

あとは前にやった MacBook Proの充電器の情報をメニューバーに表示するElectronアプリをつくった と同様にアプリケーション化して完成です。

icon_512x512.png

ソースコードはこちら(https://github.com/narikei/cough-switch)

おわりのおわり

デフォルトだとスピーカーの設定しかできないけど、
こういうときこそタッチバーの出番なのではないかな?
バックグラウンドのアプリでタッチバーを使えないのどうにかしてほしい。

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

ChromiumではないMS Edgeで要素ドラッグしようとするとタッチジェスチャーが動作してしまう件の回避

概要

  • 最新のChromium版のMicrosoft Edgeではない、古いEdge
  • タッチディスプレイで、タッチ操作

という条件で、HTML内の要素ドラッグをしようとすると、全体のタッチジェスチャー(?)が効いてドラッグできません。
Microsoft-Edge-2020-05-28-11-08-20_Trim.gif

上記キャプチャのURLはこちら。
https://gijgo.com/draggable

解決法

CSSで、ドラッグしたい要素に touch-action: none; を指定するだけ!

ひとまず、DeveloperToolで追加
image.png

うまくいった!
Microsoft-Edge-2020-05-28-11-14-18_Trim.gif

参考

https://stackoverflow.com/questions/47495465/how-to-disable-swipe-to-go-back-in-microsoft-edge-with-javascript-or-jquery

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

jest の spy を使ったテストでテスト順番に依存して落ちたり落ちなかったりする問題の解決

spy で toBeCalled() を使ったテストで、テストの順番に依存してうまく動いたり動かなかったりする現象があって、ハマりそうだったので記録しておきます。
テスト対象は TypeScript の関数ですが、型がある以外は JavaScript に読み替えても同じだと思います。

テスト対象の関数が以下のようにあるとします。

export const targetFunction = (flag: boolean) => {
  if (flag) {
    console.error('出力です')
  }
}

失敗するパターンのテスト

import { targetFunction } from '~/plugins/axios'

const spyConsoleError = jest.spyOn(console, 'error')
spyConsoleError.mockImplementation(() => {
})

describe('spy resetの動作テスト', () => {
  test('console error が呼ばれる', async () => {
    await targetFunction(true)

    expect(console.error).toBeCalled()
  })

  test('console error が呼ばれないはず', async () => {
    await targetFunction(false)

    expect(console.error).not.toBeCalled()
  })
})

これを実行してみると
image.png

対象のテストの中では console.error は呼ばれていないはずなのですが、すでに前のテストで呼ばれているため、1回呼ばれたことになってしまっています。

うまくいくように書き換えたパターン


import { targetFunction } from '~/plugins/axios'

const spyConsoleError = jest.spyOn(console, 'error')
spyConsoleError.mockImplementation(() => {
})

// ------  これを書き足します --------
beforeEach(() => {
  spyConsoleError.mockReset()
})
// ------  ここまで  ---------

describe('spy resetの動作テスト', () => {
 // 同じテスト
})

それぞれのテストの前に beforeEach で初期化処理をしてあげると依存関係の問題が起こりません。

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

knex.js + postgresql migration メモ

knex.js を postgresql で利用する際の migration について、こうやるといいんじゃないかな的な話題をまとめていきます。(随時更新予定)

bigint な primary key

64bit にしたい場合。

exports.up = function(knex) {
    return knex.schema
        .createTable('xxxx', function (table) {
            table.specificType('id', knex.raw('BIGSERIAL PRIMARY KEY'));
        });
};

enum の up / down

up / down の行き来によって enum 型定義だけ残るので強制的に消しに行ってます。

const drop_type_stmt = 'drop type if exists xxx_type';

exports.up = function(knex) {
    return knex.schema
        .raw(drop_type_stmt)
        .createTable('xxx', function (table) {
            table.enu('state', ['a', 'b'], { useNative: true, enumName: 'xxx_type' });
        });
};

exports.down = function(knex) {
    return knex.schema
        .raw(drop_type_stmt);
};
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Javascript コーディング問題(手助け求めます)

 Javascript のコーディング質問

Javascriptにて

あなたは現在価格mドルの土地の購入するために、年利i(0 < i < 100)%の金融商品にpドル投資しました。何年後に土地の購入ができるでしょうか?howLongToReachFundGoalという関数を再帰によって作成してください。なお、毎年得られた利益は同商品に再投資するとし、土地の価格は経過する年数が偶数(0を含む)の時は2%、奇数の時は3%上昇します。また、人の寿命は80歳未満と仮定し、80年以上かかる時は80としてください。

という内容の元プログラムを組んでいます。

問題に直面しているのですが、一定数の入力だと正解の出力より1多い数値を出してしまうのですが、改善方法がわからず行き詰まってしまっています。

(例)
入力:5421,10421,5
正解の出力:27
自分の出力:28

どなたか分かる方手助けお願いします。

function howLongToReachFundGoalHelper(capitalMoney, goalMoney, interest, x){
    function capitalAfterInterest(capitalMoney, interest, x){
        if(x == 0){
            return capitalMoney;
        } else {
            return capitalMoney * (1 + (interest / 100));
        }
    }
    function goalMoneyAfterInflation(goalMoney, x){
        if((x % 2 == 0) || (x == 0)){
            return goalMoney * 1.02;
        } else {
            return goalMoney * 1.03;
        }
    }
    if(x >= 80){
        return 80;
    } else if(capitalMoney >= goalMoney){
        return x;
    } else {
        return howLongToReachFundGoalHelper(capitalAfterInterest(capitalMoney, interest, x + 1), goalMoneyAfterInflation(goalMoney, x + 1), interest, x + 1);
    }
}

function howLongToReachFundGoal(capitalMoney,goalMoney,interest){
    //ここから書きましょう
    return howLongToReachFundGoalHelper(capitalMoney, goalMoney, interest, 0);
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React ContextでuseReducerを使ってみる

はじめに

今回はContext APIを用いた既存のプロジェクトにHooksの一つであるuseReducerを用いてreducerを追加していきます。ドキュメントを読んでもいまいち理解できない方向けの記事にしましたので細かい説明は省いています。そのままコピペしてみても良いかもしれません。

useReducerとは何か

(ContextAPIを用いて作られたグローバルなstateに対して)reducerを設定できるReact Hooksの一つです。つまりContextでreducerを使えます。ただ、そもそもreducerとは何か。

reducerとは何か

もともとはReduxにおいて出てくる考え方で、グローバルなstateの状態を管理する場所です。ローカルなstateの更新はstateを設定したコンポーネント内で出来ますが(下の図参照)、reduxで作成するグローバルなstateはコンポーネント側が直接変更することは出来ません。各コンポーネントはreducerに対して、『この処理を行いたい』という申請を送ると、reducerが代わりにstateの値を更新します。

App.js
import React, { useState } from 'react';

const App () => {
    const = [data, setData] = useState('before');

    const handleClick = () => {
        setData('after');
    };

    return (
        <div className="app">
            <div>{ data }</div>
            <button onClick={handleClick}>Change state</button>
        </div>
    )
};

export default App;

useReducerの使い所

ContextAPIを用いて作られたグローバルなstateの状態を管理する場所としてreducerを設定します。reduxと違いContextはreducerを必要といないため、Context内でstateを更新する関数を定義しても良いのですが、大きなアプリケーションを作る際はstateとそれを更新する関数をreducerを使って分けた方が見やすいコードになり、何よりContextのコード量を減らせます。それでは実際にContextでreducerを設定しましょう。

Context × Reducer

useReducerを追加前のプロジェクト

まずはグローバルなstateであるContextを持つ簡単なReactプロジェクトを作成します。ファイル構成、コードは以下の通りです。以下のプロジェクトにuseReducerを用いてContextにreducerを追加します。
react-app-files.jpg

とりあえずはHooksのuseStateを用いてローカルなstateを作成し、Providerにstateを渡しグローバルなstateを作成します。

contexts/UserContext.js
import React, { createContext, useState } from 'react';

const initialUsers = [
    { name: 'Taro', age: 20, id: 1 },
    { name: 'Kai', age: 18, id: 2 },
    { name: 'Ryo', age: 23, id: 3 }
]

export const UserContext = createContext();

const UserContextProvider = ({children}) => {
    const [users, setUsers] = useState(initialUsers);

    // もちろん下のようにstateの状態を管理する関数もreducerなしで作れます。
    const deleteUser = (id) => {
        const newUsers = users.filter(user => user.id !== id)
        setUser([
            ...newUsers
        ])
    }
    return (
        <UserContext.Provider value={{users, deleteUser}}>
            {children}
        </UserContext.Provider>
    )
}

export default UserContextProvider;

子コンポーネント(UserList)をProviderで囲んであげます。

src/App.js
import React from 'react';
import UserContextProvider from './context/UserContext';
import UserList from './components/UserList';

const App = () => {
    return (
        <UserContextProvider>
            <UserList />
        </UserContextProvider>
    );
}

export default App;

stateの中身を簡単に表示させます。

components/UserList.js
import React, { useContext, Fragment } from 'react';
import { UserContext } from '../context/UserContext';

const UserList = () => {
    const { users, deleteUser } = useContext(UserContext);
    const userList = users.map(user => {
        return (
            <section key={user.id}>
                <h3>{ user.name }</h3>
                <div>年齢:{ user.age }</div>
                <button 
                type="button" 
                onClick={() => deleteUser(user.id)}
                >ユーザーを消去</button>
            </section>
        )
    })
    return (
        <Fragment>
            <h2>ユーザーリスト</h2>
            {userList}
        </Fragment>
    );
}

export default UserList

結果として、ブラウザ上ではこのように表示されます。(黒の枠線はありません。)
Untitled Diagram (10).jpg

useReducerを追加後のプロジェクト

それではuseReducerを用いてContextにreducerを追加します。useReducerを使う前に、reducerを作ります。まずはsrcの下にreducersフォルダを作成し、その中に実際のreducerとなるUserReducer.jsを作ります。dispatch関数を用いて送られてきたオブジェクトをもとにstateを操作します。返り値には更新後のstateを返しています。これだけでは掴みにくいと思うので下のコードで変更した部分を確認してみてください。

src/reducers/UserReducer.js
// 下のuserReducerの引数actionは
// dispatch関数を用いて送られてきたオブジェクトが入ります
export const userReducer = (state, action) => {
    switch (action.type) {
        case 'DELETE_USER':
            // 今回はありませんがusersを含む全てのstateを返し
            // その後usersの中身を更新しています
            return {
                ...state,
                users: [
                    ...action.payload
                ]
            }
        default:
            return state
    }
}
contexts/UserContext.js
import React, { createContext, useReducer } from 'react';
import { userReducer } from '../reducers/UserReducer';

// 変更部分。分かりやすくするためにオブジェクトとします。
const initialState = {
    users: [
        { name: 'Taro', age: 20, id: 1 },
        { name: 'Kai', age: 18, id: 2 },
        { name: 'Ryo', age: 23, id: 3 }
    ]
}

export const UserContext = createContext();

const UserContextProvider = ({children}) => {
    // 変更部分。useReducerに使いたいreducer、stateの初期値を入れます。
    // useStateと似ていてstateとdispatch関数を返します。
    const [state, dispatch] = useReducer(userReducer, initialState);

    // 変更部分。stateとdispatch関数をグローバルで使えるようにします。
    return (
        <UserContext.Provider value={{state, dispatch}}>
            {children}
        </UserContext.Provider>
    )
}

export default UserContextProvider;
components/UserList.js
import React, { useContext, Fragment } from 'react';
import { UserContext } from '../context/UserContext';

const UserList = () => {
    // 変更部分。
    const { state: { users }, dispatch } = useContext(UserContext);

    // 変更部分。引数のidを用いて、そのidのユーザーを除いたnewUsersを作成。
    // dispatch関数を呼び出し、typeを指定し、ペイロードとしてnewUsersを入れる。
    const deleteUser = (id) => {
        const newUsers = users.filter(user => user.id !== id);
        dispatch({
            type: 'DELETE_USER',
            payload: newUsers
        })
    }

    const userList = users.map(user => {
        return (
            <section key={user.id}>
                <h3>{ user.name }</h3>
                <div>年齢:{ user.age }</div>
                <button 
                type="button" 
                onClick={() => deleteUser(user.id)}
                >ユーザーを消去</button>
            </section>
        )
    })
    return (
        <Fragment>
            <h2>ユーザーリスト</h2>
            {userList}
        </Fragment>
    );
}

export default UserList

※App.jsの変更はありません。
Untitled Diagram (10).jpg

ご覧の通りuseReducer追加前のものと同じ機能を持つアプリができました。これで完成です!

最後に

ここまで読んでいただきありがとうございます。誤字、説明等の間違いがあった場合コメントください。

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

JavaScript基本文法(初学者向け)

コンソールにテキストを表示

console.log() 
引数として()内にコンソールに表示したい情報を渡します。変数を渡すことも可能。

変数の宣言

let name = 'sasaki'
const name = 'ken'
変数の宣言はletを使用。後で書き換え可能の宣言はlet
書き換え不能の宣言はconstを使用します。

条件分岐

if (条件式1) {
  // 条件式1がtrueのときの処理
} else if (条件式2) {
  // 条件式1がfalseで条件式2がtrueのときの処理
} else {
  // 条件式1も条件式2もfalseのときの処理
}

条件式は()で括ること。
条件に対しての処理は{}で括ること。
複数の条件が必要の場合は、elseの後にifを書きます。

 配列

let list = ['Ruby', 'Ruby on Rails', 'JavaScript', 'HTML', 'CSS'];

配列定義する場合は[]内に記述 。
要素番号は0からスタートで、この場合はRubyが0番目の要素です。

この配列に対して以下の4つの操作を紹介▼

①配列の要素を取得する
▶︎配列の要素を取得するにはconsole.log(配列を代入した変数名[要素番号])で取得。

②配列の要素数を取得する
▶︎配列の要素数を取得するにはconsole.log(配列を代入した変数名.length)で取得。

③配列の要素を追加する
▶︎配列の要素を追加するには、push.配列を代入した変数名(追加したい要素)で追加。

④配列の要素を削除する
▶︎配列の要素を削除するには、配列を代入した変数名.pop()で末尾の要素を削除。先頭の要素を削除する場合は、shiftメソッドを使用。なお、JavaScriptでは取り除く要素を指定できない模様。

オブジェクト

オブジェクトを定義するにはlet 変数名 = {オブジェクト名: 値}で定義。
オブジェクトは名前と値にて情報を管理しており、それをプロパティと呼ぶ。

オブジェクトに対して以下の2つの操作を紹介。

①プロパティの値を取得する
▶︎プロパティの値を取得するには、console.log(オブジェクトを代入した変数名.オブジェクト名)で取得する。

②プロパティの値を変更する
▶︎プロパティの値を変更するには、オブジェクトを代入した変数名.オブジェクト名 = 新たに変更したい値で変更する。

繰り返し処理

繰り返し処理for文を使用します。基本的な構文は下記の通りです。

for (let i = 0; i < 繰り返す回数; i = i + 1) {
  // 繰り返す処理の内容
}

i = i + 1の部分はiに1を追加した数値が表示されます。なおi = i + 1の部分は繰り返す毎に呼ばれます。i += 1に省略可能。

関数宣言と関数式

#関数宣言
function 関数名(引数) {
  // 処理の内容
}

#関数式(無名関数)
let hello = function(){
    console.log('hello');
}

関数宣言と関数式の役割に大きな違いはありません。
ただし、関数式の方が何かに代入したり、他の関数に渡したりしやすい傾向にあります。
また、関数に引数が含まれない場合でも()は記述します。

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

GatsbyにTableOfContents(目次)をつける

はじめに

本記事は https://tech-blog.yoshikiohashi.dev/posts/start-gatsby-blog-tableofcontent のクロスポスト記事になります。

この記事はGatsbyというヘッドレスCMS技術で構成されています。今回は「エンジニア初心者でもできる」を前提に以下の構成で記事を作成していこうと思います。

内容

前回はタグ一覧と記事一覧のコンポーネントを同時に出すGrapuQLクエリーの応用まで行いました。

今回はブログで欠かせないTableOfContents(目次)の実装方法のご紹介です。全く難しくないのでササッと行きましょう!

クエリー

超簡単です。記事を取得しているクエリーにtableOfContentsを付け足すだけです。エディタで結果を確認してみましょう。

  query PostBySlug($slug: String!) {
    markdownRemark(fields: { slug: { eq: $slug } }) {
      id
      html
      fields {
        slug
        tagSlugs
      }
      frontmatter {
        date
        description
        tags
        title
        socialImage
      }
      tableOfContents
    }
  }

このようにHTML形式のデータを取得することができました。あとは表示するだけです。

コンポーネント作成

TOCを表示するコンポーネントつくりです。

ちなみになぜコンポーネントにするのか?理由は単純で

  • 使い回しがしやすい
  • CSSの設定を限定的にできる
  • パーツを好きなところに配置しやすくなる

からです。パーツ1つ1つの依存度を下げていきましょう。

下のようにdangerouslySetInnerHTML={{ __html: tableOfContents }}に先程取得したHTMLデータを流し込みましょう。(CSSの設定はお好みで設定してください)

const Toc = ({ tableOfContents, gridArea }: Props) => (
  <div className={styles.toc} dangerouslySetInnerHTML={{ __html: tableOfContents }} />
);

export default Toc;

まとめ

いかがだったでしょうか?

他にもgatsby-remark-tocなどのライブラリがあるみたいですが、私個人としてはこちらの方がシンプルで簡潔だと考えています。それでは次回の記事で。

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

Gatsbyにタグ機能、カテゴリ機能をつける(応用編)

はじめに

本記事は https://tech-blog.yoshikiohashi.dev/posts/start-gatsby-blog-add-tags-application のクロスポスト記事になります。

この記事はGatsbyというヘッドレスCMS技術で構成されています。今回は「エンジニア初心者でもできる」を前提に以下の構成で記事を作成していこうと思います。

内容

前回はGraphQLを使用して、データを取得したJsonデータをTemplateに当てはめて画面に表示させるところまで行いました。 データの流れを理解できたでしょうか?この流れを理解するとあとは簡単です。

今回は応用編です。

「実際にはタグだけのページって必要でしょうか?」私は必要ないと思います。できれば、ユーザとしては選んだタグと記事の一覧が見れたら使いやすいはずです。

こんなタグ一覧+記事一覧ページを作るところをゴールに目指しましょう。

まずはタグ一覧+記事一覧のテンプレートファイルの作成

最初にGraphQLのクエリーを完成させよう

ポイントになってくるのがquery TagsListTemplate($tag: String!)tags: { glob: $tag }のTemplateの引数の指定の仕方と使い方です。globはglobalの略でいわゆる何でも検索ができます。*を入れたらすべてのタグが見つかるようになります。

タグ一覧(/tags)では全タグを表示させたいので*が入ってきますね。

query TagsListTemplate($tag: String!) {
    allMarkdownRemark(
        filter: { 
            frontmatter: { 
                template: { eq: "post" },
                draft: { ne: true }, 
                tags: { glob: $tag } } 
        },
        sort: { order: DESC, fields: [frontmatter___date] }
    ){
        group(field: frontmatter___tags) {
            fieldValue
            totalCount
        }
        edges {
            node {
                fields {
                    slug
                    categorySlug
                }
                frontmatter {
                    title
                    date
                    category
                    description
                    socialImage
                }
                excerpt
            }
        }
    }
}

あとは、記事を取得するクエリーとタグを取得するクエリーを記述しましょう。

group(field: frontmatter___tags) {
    fieldValue
    totalCount
}
edges {
    node {
        fields {
            slug
            categorySlug
        }
        frontmatter {
            title
            date
            category
            description
            socialImage
        }
        excerpt
    }
}

GraphQLを実際に http://localhost:8000/___graphql で試してJsonが取得できたら次へ進みましょう。

次は、取得したJsonをComponentに当てはめていきます。

const TagsListTemplate = ({ data, pageContext }) => {
  const tags = data.allMarkdownRemark.group;
  const posts = data.allMarkdownRemark.edges;

  return (
    <div className={TagsList}>
      <Tags tags={tags} selectedTag={pageContext.tag}/>
      <Post posts={posts} /> // それぞれの記事を表示するコンポーネントに合わせて差し替えてください
    </div>
  );
};

export const query = graphql`
    query TagsListTemplate($tag: String!) {
        allMarkdownRemark(
            filter: { frontmatter: { template: { eq: "post" }, draft: { ne: true }, tags: { glob: $tag } } },
            sort: { order: DESC, fields: [frontmatter___date] }
        ){
            group(field: frontmatter___tags) {
                fieldValue
                totalCount
            }
            edges {
                node {
                    fields {
                        slug
                        categorySlug
                    }
                    frontmatter {
                        title
                        date
                        category
                        description
                        socialImage
                    }
                    excerpt
                }
            }
        }
    }
`;

export default TagsListTemplate;

ここまででGraphQLクエリーでタグと記事の一覧を取得するクエリーを書き、Componentに当てはめることまでしました。次はタグのコンポーネントの作成です。

タグ一覧を表示するコンポーネントパーツの作成

先程作成したTemplateから呼び出されるタグコンポーネントです。こちらの例ではカウント順にソートしていますが、してもしなくてもOKです。

また、引数にselectedTagがありますが、選択している状態を表現したかったので表示しています。好みで変更してOKです。

const sortTotalCount = (tags) => orderBy(tags, ['totalCount', 'fieldValue'], ['desc']);

const Tags = ({ tags, selectedTag }: Props) => (
  <div className={styles["Tags"]}>
    {sortTotalCount(tags).map(tag => (
      <li key={tag.fieldValue}>
        <Link to={`/tags/${kebabCase(tag.fieldValue)}/`} className={selectedTag === tag.fieldValue ? styles['Tags--Selected'] : '' }>
          {tag.fieldValue}
          <span>{tag.totalCount}</span>
        </Link>
      </li>
    ))}
  </div>
);

export default Tags;

gatsby-node.js に /tags//tags/{tags} のルーティングを作成する

gatsby-node.js

まずはタグ一覧ページを登録します。

すべてのタグを表示するのでcontextに{ tag: "*" }を指定しています。

  // Tags list
  createPage({
    path: '/tags',
    component: path.resolve('./src/templates/tags-list-template.js'),
    context: { tag: "*" }
  });

タグが選ばれた場合のURLを登録していきます。

ポイントはcontextの{ tag: tag.fieldValue }を指定している箇所です。これでURLが/tags/{tags}だったときにタグ名がTemplateに引数として受け渡しされることになります。

  const result = await graphql(`
    {
      allMarkdownRemark(
        filter: { frontmatter: { template: { eq: "post" }, draft: { ne: true } } }
      ) {
        group(field: frontmatter___tags) {
          fieldValue
          totalCount
        }
      }
    }
  `);

  _.each(result.data.allMarkdownRemark.group, (tag) => {
    const numPages = Math.ceil(tag.totalCount / postsPerPage);
    const tagSlug = `/tags/${_.kebabCase(tag.fieldValue)}`;

    for (let i = 0; i < numPages; i += 1) {
      createPage({
        path: i === 0 ? tagSlug : `${tagSlug}/page/${i}`,
        component: path.resolve('./src/templates/tags-list-template.js'),
        context: {
          tag: tag.fieldValue
        }
      });
    }
  }

処理の流れを表すとこのような図になります。

gatsby build を再度実行

/tags のページに遷移してみましょう。タグ一覧とその記事が表示されましたか?

まとめ

いかがだったでしょうか?うまく設定できたでしょうか?ここまで理解できたら他の実装にも応用できますね。それでは次回の記事で。

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

Gatsbyにタグ機能、カテゴリ機能をつける(基礎編)

はじめに

本記事は https://tech-blog.yoshikiohashi.dev/posts/start-gatsby-blog-add-tags のクロスポスト記事になります。

この記事はGatsbyというヘッドレスCMS技術で構成されています。今回は「エンジニア初心者でもできる」を前提に以下の構成で記事を作成していこうと思います。

内容

今回はWordPressのタグ機能、カテゴリ機能に当たる部分を実装していきます。標準で実装されてないの?!って突っ込みはあるかと思いますが、そうです。標準のテンプレートでは実装されておりません。なので実装するよりも最初からあるテンプレートを選んだほうが良いです。

テンプレートのおすすめはこちら

実装する前に

https://www.gatsbyjs.org/docs/adding-tags-and-categories-to-blog-posts/

まずこちらの記事を読みましょう。GatsbyJS本家でタグの付け方について記載されています。

手順

  1. markdownファイルにタグを追加する
  2. GraphQLクエリを作成し、全てのタグを取得する
  3. タグページテンプレートを作成する(/tag/{tag}
  4. 作成したテンプレートを使用して、gatsby-node.jsでページをレンダリングする
  5. すべてのタグのリストを表示するタグインデックスページを作成する(/tags

1.markdownファイルにタグを追加する

タグがない場合のマークダウンファイル。ない場合追加する必要があります。

---
title: "A Trip To the Zoo"
---

I went to the zoo today. It was terrible.

tagsという名前で項目を追加しました。タグは複数個設定される想定なので、配列として定義します。他にも、文字列、数値が設定できます。(カテゴリも一緒で単一のため文字列として設定します。)

---
title: "A Trip To the Zoo"
tags: ["animals", "Chicago", "zoos"]
---

I went to the zoo today. It was terrible.

ローカル環境でgatsby developが実行されている場合は、再起動すると、Gatsbyが新しい項目を取得できるようになります。

2. GraphQLクエリを作成し、全てのタグを取得する

GraphQLを確認するためにはローカルでgatsby developを実行し、http://localhost:8000/___graphqlにアクセスします。

下のように画面が見えるはずです。

画面が表示されたら下のクエリーを入力してみましょう。

{
  allMarkdownRemark {
    group(field: frontmatter___tags) {
      tag: fieldValue
      totalCount
    }
  }
}

タグ一覧が取得できるはずです。

ちなみにgroup()はSQLのGroupByと同じような意味合いです。ここではタグ項目でグルーピングしています。

3. タグページテンプレートを作成する(/tag/{tag}

ディレクトリはsrc/template/tags.jsなどの配置にしましょう。

import React from "react"
import PropTypes from "prop-types"

// Components
import { Link, graphql } from "gatsby"

const Tags = ({ pageContext, data }) => {
  const { tag } = pageContext
  const { edges, totalCount } = data.allMarkdownRemark
  const tagHeader = `${totalCount} post${
    totalCount === 1 ? "" : "s"
  } tagged with "${tag}"`

  return (
    <div>
      <h1>{tagHeader}</h1>
      <ul>
        {edges.map(({ node }) => {
          const { slug } = node.fields
          const { title } = node.frontmatter
          return (
            <li key={slug}>
              <Link to={slug}>{title}</Link>
            </li>
          )
        })}
      </ul>
      {/*
              This links to a page that does not yet exist.
              You'll come back to it!
            */}
      <Link to="/tags">All tags</Link>
    </div>
  )
}

Tags.propTypes = {
  pageContext: PropTypes.shape({
    tag: PropTypes.string.isRequired,
  }),
  data: PropTypes.shape({
    allMarkdownRemark: PropTypes.shape({
      totalCount: PropTypes.number.isRequired,
      edges: PropTypes.arrayOf(
        PropTypes.shape({
          node: PropTypes.shape({
            frontmatter: PropTypes.shape({
              title: PropTypes.string.isRequired,
            }),
            fields: PropTypes.shape({
              slug: PropTypes.string.isRequired,
            }),
          }),
        }).isRequired
      ),
    }),
  }),
}

export default Tags

export const pageQuery = graphql`
  query($tag: String) {
    allMarkdownRemark(
      limit: 2000
      sort: { fields: [frontmatter___date], order: DESC }
      filter: { frontmatter: { tags: { in: [$tag] } } }
    ) {
      totalCount
      edges {
        node {
          fields {
            slug
          }
          frontmatter {
            title
          }
        }
      }
    }
  }
`

補足

基本的にsrc/template/の配下のJSファイルはGraphQLがセットになっている構成が望ましいです。(必要でなければなくても良いです。)

流れで表すと下のようなイメージです。

GraphQL == クエリ結果 => Template == クエリ結果 => Componentの各パーツ

4. 作成したテンプレートを使用して、gatsby-node.jsでページをレンダリングする

さて、テンプレートページは完成したのであとはGatsby buildをするときにタグページを読み込ませるだけです。Gatsbyは最初に指定URLのページを読み込ませてビルドすることで静的なページが作成されていきます。

ここのgatsby-node.jssrc/templates/tags.jsに対してGraphQLで取得した結果をfor文でタグ数分生成しています。

const path = require("path")
const _ = require("lodash")

exports.createPages = async ({ actions, graphql, reporter }) => {
  const { createPage } = actions

  const blogPostTemplate = path.resolve("src/templates/blog.js")
  const tagTemplate = path.resolve("src/templates/tags.js")

  const result = await graphql(`
    {
      postsRemark: allMarkdownRemark(
        sort: { order: DESC, fields: [frontmatter___date] }
        limit: 2000
      ) {
        edges {
          node {
            fields {
              slug
            }
            frontmatter {
              tags
            }
          }
        }
      }
      tagsGroup: allMarkdownRemark(limit: 2000) {
        group(field: frontmatter___tags) {
          fieldValue
        }
      }
    }
  `)

  // handle errors
  if (result.errors) {
    reporter.panicOnBuild(`Error while running GraphQL query.`)
    return
  }

  const posts = result.data.postsRemark.edges

  // Create post detail pages
  posts.forEach(({ node }) => {
    createPage({
      path: node.fields.slug,
      component: blogPostTemplate,
    })
  })

  // Extract tag data from query
  const tags = result.data.tagsGroup.group

  // Make tag pages
  tags.forEach(tag => {
    createPage({
      path: `/tags/${_.kebabCase(tag.fieldValue)}/`,
      component: tagTemplate,
      context: {
        tag: tag.fieldValue,
      },
    })
  })
}

5. すべてのタグのリストを表示するタグインデックスページを作成する(/tags

今度はタグ一覧ページを作成していきます。前に書いたGraphQLクエリーでタグ一覧を取得し、Templateに取得結果を当てはめていきます。

import React from "react"
import PropTypes from "prop-types"

// Utilities
import kebabCase from "lodash/kebabCase"

// Components
import { Helmet } from "react-helmet"
import { Link, graphql } from "gatsby"

const TagsPage = ({
  data: {
    allMarkdownRemark: { group },
    site: {
      siteMetadata: { title },
    },
  },
}) => (
  <div>
    <Helmet title={title} />
    <div>
      <h1>Tags</h1>
      <ul>
        {group.map(tag => (
          <li key={tag.fieldValue}>
            <Link to={`/tags/${kebabCase(tag.fieldValue)}/`}>
              {tag.fieldValue} ({tag.totalCount})
            </Link>
          </li>
        ))}
      </ul>
    </div>
  </div>
)

TagsPage.propTypes = {
  data: PropTypes.shape({
    allMarkdownRemark: PropTypes.shape({
      group: PropTypes.arrayOf(
        PropTypes.shape({
          fieldValue: PropTypes.string.isRequired,
          totalCount: PropTypes.number.isRequired,
        }).isRequired
      ),
    }),
    site: PropTypes.shape({
      siteMetadata: PropTypes.shape({
        title: PropTypes.string.isRequired,
      }),
    }),
  }),
}

export default TagsPage

export const pageQuery = graphql`
  query {
    site {
      siteMetadata {
        title
      }
    }
    allMarkdownRemark(limit: 2000) {
      group(field: frontmatter___tags) {
        fieldValue
        totalCount
      }
    }
  }
`

カテゴリーの実装

カテゴリー機能も考え方はタグと一緒です。タグは1つの記事に対し複数個の配列で構成され、カテゴリーは1記事に対して、単一の項目なので文字列としてマークダウンファイルに記述する必要があります。

実装手順

  1. markdownファイルに文字列型のカテゴリーを追加する
  2. GraphQLクエリを作成し、全てのカテゴリーを取得する
  3. カテゴリーページテンプレートを作成する
  4. 作成したテンプレートを使用して、gatsby-node.jsでページをレンダリングする
  5. すべてのカテゴリーのリストを表示するインデックスページを作成する

実装内容はほぼ一緒なので割愛します

まとめ

いかがだったでしょうか?ちょっと今回は難しかったでしょうか?このブログのソースコードは公開されているので良ければ参考にどうぞ。それでは次回の記事で。

https://github.com/yoshiki-0428/engneer-blog/blob/master/src/templates/tags-list-template.js#L29-L53

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

GatsbyにShare機能、OGPタグをつける

はじめに

本記事は https://tech-blog.yoshikiohashi.dev/posts/start-gatsby-blog-share/ のクロスポスト記事になります。

この記事はGatsbyというヘッドレスCMS技術で構成されています。今回は「エンジニア初心者でもできる」を前提に以下の構成で記事を作成していこうと思います。

内容

今回はWordPressのシェアボタンなんかでよくあるSNSへのシェアボタンとOGP設定タグの付け方を解説していきます。

SNSへのシェアボタン

OGP設定タグ

SNSシェアボタンを実装する

react-shareのインストール

めっちゃ簡単です。まずライブラリをインストール。

yarn add react-share

ShareSns.jsのコンポーネントの作成

適当なディレクトリを作成して(ここではShareSnsとします)

export const ShareSns = ({ articleUrl, articleTitle }) => (
  <div className={'ShareSns'}>
    <div>
      <FacebookShareButton url={articleUrl}>
        <FacebookIcon size={32} round />
      </FacebookShareButton>

      <LineShareButton url={articleUrl}>
        <LineIcon size={32} round />
      </LineShareButton>

      <LinkedinShareButton url={articleUrl}>
        <LinkedinIcon title={articleTitle} size={32} round />
      </LinkedinShareButton>

      <TwitterShareButton title={articleTitle} via="yoshiki__0428" url={articleUrl}>
        <TwitterIcon size={32} round />
      </TwitterShareButton>
    </div>
  </div>
);

呼び出し方

ちょっと呼び出し方がもどかしいですが、Gatsby buildをするときにwindow.location.hrefが未定義なのでビルド時に落ちてしまいます。そのため、typeofで確認する必要があります。

{typeof window !== 'undefined' && window.location.href &&
  <ShareSns articleUrl={window.location.href} articleTitle={title} />
}

もしくは呼び出し前にundifinedチェックをして値が存在するか事前に確認しても良いと思います。

const windowUrl = (typeof window !== 'undefined' && window.location.href) ? window.location.href : '';

これで下のようなボタンが表示されるはずです。他にも色々なSNSのボタンがあるので使ってみてはいかがでしょうか。

OGP設定タグ

react-helmetのインストール

大抵のTemplateには入っているのでやらなくても良いかもです。

yarn add react-helmet

Metaタグ専用のコンポーネントの作成

使い方は簡単です。Helmetタグを作成し、そこに必要なMetaタグを入力すればOKです。

この例ではOGとTwitterのOGPを設定しています。呼び出し元は画像データやタイトルデータが取得できる記事画面で呼び出すと良いと思います。Templateによって設計がまちまちなので環境に合わせて適用してみてください。

<Helmet>
  <html lang="jp" />
  <title>{title}</title>
  <meta name="description" content={description} />
  <meta property="og:site_name" content={title} />
  <meta property="og:image" content={image} />
  <meta name="twitter:card" content="summary" />
  <meta name="twitter:title" content={title} />
  <meta name="twitter:description" content={description} />
  <meta name="twitter:image" content={image} />
</Helmet>

OGPタグの設定確認

netlifyにアップロードしたらTwitterのカード情報が確認できるサイトで対象のURLを入力して確認してみましょう。設定できていればOKです。Slackのチャットなんかにも投稿しても確認できます。

まとめ

いかがだったでしょうか?結構簡単に設定できたんじゃないかなと思います。ここらへんの機能もブログをやるのであれば必須なので是非やっておきましょう。それでは次回の記事で。

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

Puppeteerを2系から3系にアップデートしたらError: Failed to launch the browser process!

Puppeteerメモです。

Heroku上で利用していたPuppetterのバージョンを2系->3系にアップデートしたらエラー発生。

Puppetterのバージョンを2系->3系にするとChromiumのバージョン違いで動かないっぽい

Heroku上のログ

Error: Failed to launch the browser process!
2020-05-27T21:22:10.217234+00:00 app[web.1]: /app/node_modules/puppeteer/.local-chromium/linux-756035/chrome-linux/chrome: error while loading shared libraries: libgbm.so.1: cannot open shared object file: No such file or directory

Heroku上でインストールされているChromiumを更新する

https://github.com/puppeteer/puppeteer/releases

Puppetterのv3.1.0ではChromium 83.0.4103.0 (r756035)を利用していると、Big changesと書かれていて、内部で利用するChromiumのバージョンが違う模様です。

最初にインストールしたときはPuppetterのv2系だったので、その時に対応していたバージョンのChromiumがHeroku側に残ったままになってるんだと思います。

bildpackを再ビルド

$ heroku buildpacks:add jontewks/puppeteer

その後デプロイしなおす(git push heroku master)と再度ビルドされてHeroku上のChromiumも更新されてこれで問題なく利用できるようになりました。

その他メモ

その他触っててでたエラーをメモ的に残しておきます。

TimeoutError: Navigation timeout of 30000 ms exceeded

タイムアウト。
デフォルトだと30秒待ってくれるみたいだけど、それ以上かかる場合もありそう。

page.goto()のオプションで{waitUntil: 'networkidle'}を指定するっぽいけど更にエラーが

Error: ERROR: "networkidle" option is no longer supported. Use "networkidle2" instead

なるほど

page.goto(url, {waitUntil: 'networkidle2'})

でうまくいった。

Error: Evaluation failed: TypeError: Cannot set property 'value' of null

こんな雰囲気でiPhoneでのページ閲覧エミュレートをしてましたが、

const devices = require('puppeteer/DeviceDescriptors');
const iPhone = devices['iPhone X'];

省略

await page.emulate(iPhone);

これもv2->3にあげたらError: Evaluation failed: TypeError: Cannot set property 'value' of nullとエラーが発生し、うまく動作しなくなってしまいました。

これパッと調べても原因や解決策がわからなかったので、iPhoneでのエミュレートを泣く泣く断念。

今回やりたかったのはデスクトップエミュレートでもなんとかなったのでよかったけど、必要になったらまた調べないと...

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

Firebaseの技術を使ってWebページをもう少し進化させる

今回、FirebaseのRealtime DatabaseとCloud Storageの仕組みを使って今までのwebページの機能を増やしました。

Firebaseの全体図と各機能の理解については
以下が非常に参考になりました。
https://www.topgate.co.jp/firebase01-what-is-firebase

Firebaseとは

Firebase は Google が提供しているモバイルおよび Web アプリケーションのバックエンドサービスです。クラウドサービスの形態では BaaS に位置付けされます。 Firebase を使うことで、バックエンドで動くサービスを作成する必要も管理する必要がなくなり、開発者はアプリケーションの開発に専念できます。

Realtime Databaseとは

Firebase に元から含まれているオブジェクト型のデータベースです。リアルタイムでクライアント全体の状態を同期させることができ、オフラインで動作するときはデータをキャッシュしてオンラインになった時に自動的にデータを同期します。

Cloud Storage for Firebaseとは

写真や動画などバイナリーデータを保存します。保存先は Cloud Storage となっており、 Firebase と Google Cloud の両方からアクセスできます。また、スケールアウト機能も兼ね備えており、急激なアプリケーションの拡大にも対応しています。

公開Webページ

https://3dblock.jp

今回追加した機能

①ログインフォーム
ブロックで遊ぼう-Google-Chrome-2020-05-28-03-19-04.gif

②画像投稿ページ

Voxel-Ant-_-あなただけのブロックアートを。-Google-Chrome-2020-05-28-03-26-59.gif

コード

HTML

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

<head>
    <meta charset="utf-8">
    <title>Voxel Ant | あなただけのブロックアートを。</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
    <link rel="stylesheet" href="main.css">
</head>
<header>
    <link rel="stylesheet" href="main.css">
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <h1>
        <a href="index.html"><img src="ant-logo4.png" alt="街を作ろう"></a>
    </h1>
</header>

<body>
    <section id="loading" class="view">
        <i class="initial-loading-icon fas fa-sync" aria-hidden="true"></i>
    </section>
    <div id="logo-gazo">
        <img src="ant-logo3.png">
    </div>
    <!-- /#ログイン画面 -->

    <section id="login" class="view">
        <div class="container">
            <div class="row justify-content-center">
                <div class="col-sm-10 col-md-8 col-lg-6">
                    <form id="login-form">
                        <div class="form-group login__email">
                            <label for="login-email" class="col-form-label">
                                メールアドレス
                            </label>
                            <div>
                                <input id="login-email" type="email" class="form-control" required>
                            </div>
                        </div>
                        <div class="form-group login__password">
                            <label for="login-password" class="col-form-label">
                                パスワード
                            </label>
                            <div>
                                <input id="login-password" type="password" class="form-control" required>
                            </div>
                        </div>
                        <div id="login__help" class="alert alert-danger"></div>
                        <div class="form-group login__submit">
                            <div>
                                <button id="login__submit-button" type="submit" class="btn btn-success">
                                    ログイン
                                </button>
                            </div>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </section>
    <!-- /#ログイン画面 -->

    <!-- 一覧画面 -->
    <section id="blcokgallery" class="view">
        <header>
            <div id="header">
                <a href="#add-block-modal" data-toggle="modal" class="add-button">
                    <i class="fas fa-plus-circle" aria-hidden="true"></i> ブロックアートの登録
                </a>
                <button class="btn btn-primary logout-button">
                    ログアウト
                </button>
            </div>
        </header>
        <div id="cover">
            <h1 id="cover__title">ブロックアートギャラリー</h1>
        </div>
        <div class="wrapper">
            <div id="main">
                <div id="block-list" class="clearfix"></div>
            </div>
        </div>
    </section>

    <div id="block-template">
        <div class="block-item">
            <div class="block-item__image-wrapper">
                <img class="block-item__image" alt="">
            </div>
            <div class="block-item__detail">
                <div class="block-item__title"></div>
                <div class="block-item__delete-wrapper">
                    <button class="btn btn-danger block-item__delete">
                        <i class="fas fa-trash-alt" aria-hidden="true"></i>
                    </button>
                    <div id="good">
                        <button-preference></button-preference>
                    </div>
                    <script src="sub1.js"></script>
                </div>
            </div>
        </div>
    </div>

    <div id="add-block-modal" class="modal fade" tabindex="-1" role="dialog">
        <div class="modal-dialog" role="document">
            <div class="modal-content">
                <div class="modal-header">
                    <h4 class="modal-title">ブロックアートの登録</h4>
                    <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                        <span aria-hidden="true">&times;</span>
                    </button>
                </div>
                <div class="modal-body m-1">
                    <form id="block-form">
                        <div class="form-group row">
                            <label for="add-block-title" class="col-md-3  col-form-label">タイトル</label>
                            <div class="col-md-9">
                                <input id="add-block-title" type="text" class="form-control" required>
                            </div>
                        </div>
                        <div class="form-group row">
                            <div class="col-md-3">アート画像</div>
                            <div class="col-md-9">
                                <div class="custom-file">
                                    <input id="add-block-image" type="file" accept=".jpg,.jpeg,.png,.gif, image/jpeg,image/png,image/gif" class="custom-file-input" required>
                                    <label id="add-block-image-label" class="custom-file-label" for="add-block-image">ファイルを選択</label>
                                </div>
                            </div>
                        </div>
                        <div id="add-block__help" class="alert alert-danger"></div>
                        <button id="submit_add_block" type="submit" class="btn btn-default btn-success btn-block">
                            保存する
                        </button>
                </div>
                </form>
            </div>
        </div>
    </div>

    <script src="https://www.gstatic.com/firebasejs/6.2.0/firebase-app.js"></script>
    <script src="https://www.gstatic.com/firebasejs/6.2.0/firebase-auth.js"></script>
    <script src="https://www.gstatic.com/firebasejs/6.2.0/firebase-database.js"></script>
    <script src="https://www.gstatic.com/firebasejs/6.2.0/firebase-storage.js"></script>
    <script>
        const firebaseConfig = {
            apiKey: "HOGEHOGE",
            authDomain: "HOGEHOGE",
            databaseURL: "HOGEHOGE",
            projectId: "HOGEHOGE",
            storageBucket: "HOGEHOGE",
            messagingSenderId: "HOGEHOGE",
            appId: "HOGEHOGE",
            measurementId: "HOGEHOGE"
        };
        firebase.initializeApp(firebaseConfig);
    </script>
    <script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script>
    <script src="main.js"></script>
</body>

</html>

補足

以下については自身のAPIキー等に置き換えてください。

    <script>
        const firebaseConfig = {
            apiKey: "HOGEHOGE",
            authDomain: "HOGEHOGE",
            databaseURL: "HOGEHOGE",
            projectId: "HOGEHOGE",
            storageBucket: "HOGEHOGE",
            messagingSenderId: "HOGEHOGE",
            appId: "HOGEHOGE",
            measurementId: "HOGEHOGE"
        };
        firebase.initializeApp(firebaseConfig);

参考にした記事・リンク

<技術編>
- https://www.topgate.co.jp/firebase01-what-is-firebase
- https://firebase.google.com/docs/database/web/read-and-write?hl=ja
- https://firebase.google.com/docs/database/web/save-data?hl=ja

<独自ドメインでの公開>
- 爆速!Vercelとfreenomで独自ドメインのサイトを無料で作成する
https://qiita.com/n0bisuke/items/901154531ea14f978bd4

終わりに

  • 今回、時間がなく出来ませんでしたが、本当はAI技術を組み合わせた実装にも挑戦してみたかったです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

昔、WEBのカラーコードを調べる時によくこういうモノで調べてました。

体内リズムが崩れてしまった、taokaです(´・ω・`)。
昔、WEBのカラーコードを調べる時によくこういうモノで調べてました。
今ではプラグインやデベロッパーツールで簡単にカラーコードが分かってしまいます。

※履歴を溜めてごめんなさい

https://taoka-toshiaki.com/qiita-no5/

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <meta name="Description" content="Enter your description here" />
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.5.0/css/bootstrap.min.css">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.13.0/css/all.min.css">
    <title>color/#Hexadecimal</title>
    <style>
        body {
            background-color: rgb(4, 6, 38);
            color: rgb(109, 135, 145);
        }
        h1{
            background-color: aliceblue;
        }
    </style>
</head>
<body onload="crgb(1)">
    <h1 class="h1 mt-5 mb-5"><a href="../">home</a></h1>
    <div class="form-group">
        <label for="myinput1">
            <h1 class="h1" style="color:red"></h1>
        </label>
        <input id="myinput1" class="form-control-range" type="range" name="" onchange="crgb()" value="4" min="0"
            max="255" step="1">
    </div>
    <div class="form-group">
        <label for="myinput2">
            <h1 class="h1" style="color: green;"></h1>
        </label>
        <input id="myinput2" class="form-control-range" type="range" onchange="crgb()" name="" value="6" min="0"
            max="255" step="1">
    </div>
    <div class="form-group">
        <label for="myinput3">
            <h1 class="h1" style="color: blue;"></h1>
        </label>
        <input id="myinput3" class="form-control-range" type="range" onchange="crgb()" name="" value="38" min="0"
            max="255" step="1">
    </div>
    <h1 class="h1" id="view">
    </h1>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.slim.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.16.1/umd/popper.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.5.0/js/bootstrap.min.js"></script>
    <script>
        function crgb(off){
            // http://www-creators.com/archives/4463
            let getParam = function (name, url) {
                if (!url) url = window.location.href;
                name = name.replace(/[\[\]]/g, "\\$&");
                var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"),
                    results = regex.exec(url);
                if (!results) return false;
                if (!results[2]) return false;
                return parseInt(decodeURIComponent(results[2].replace(/\+/g, " ")));
            };
            // https://taoka-toshiaki.com 
            if(off){
                document.getElementById("myinput1").value = getParam("r")!==false?getParam("r"):document.getElementById("myinput1").value;
                document.getElementById("myinput2").value = getParam("g")!==false?getParam("g"):document.getElementById("myinput2").value;
                document.getElementById("myinput3").value = getParam("b")!==false?getParam("b"):document.getElementById("myinput3").value;
            }
            document.body.style.backgroundColor = "rgb(" + document.getElementById("myinput1").value + "," + document.getElementById("myinput2").value + "," + document.getElementById("myinput3").value + ")";
            // https://lab.syncer.jp/Web/JavaScript/Snippet/60/
            let rgb2hex = function (rgb) {
                return "background-color:#" + rgb.map(function (value) {
                    return ("0" + parseInt(value).toString(16)).slice(-2);
                }).join("");
            };
            // https://taoka-toshiaki.com 
            document.getElementById("view").innerHTML = rgb2hex([document.getElementById("myinput1").value, document.getElementById("myinput2").value, document.getElementById("myinput3").value]);
            history.pushState(null, null, "?r=" + document.getElementById("myinput1").value + "&g=" + document.getElementById("myinput2").value + "&b=" + document.getElementById("myinput3").value);
        }
    </script>
</body>
</html>

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