20200921のHTMLに関する記事は12件です。

Twitterクローンもどき作成までの過程

PC版のTwitterのようなサイトを作ろうと思って調べたことをまとめていきます

要素をきっちり3分割

target{
 width : calc(100% / 3) ;
}

参考:【CSSで3等分】要素をきっちり三分割するスタイルシートの書き方

画面サイズ取得

画面取得するには以下を使用する
この取得したサイズに応じて表示する内容を変更したりする
どちらもスクロールサイズを除くサイズ

縦サイズ

document.documentElement.clientWidth

横サイズ

document.documentElement.clientWidth

画面サイズ変更の監視

window.addEventListener('resize',()=>{
  //処理
}

参考:要素のリサイズに応じてイベントを発動する方法

入力ボックス

入力ボックスはHTML5の contentEditable 属性を使う
この属性をtrueにすると編集できるようになる
inputを使うよりもシンプルな入力画面になる

<div contentEditable="true">入力ボックス</div>

aの下線部を消す

text-decoration: none;

Twitterのようなタイムラインの形のテンプレート

<body style="background-color:rgb(54, 3, 3)">
    <div style="background-color: rgb(107, 107, 2);width: 500px;display: inline-flex;">
        <!-- ユーザ情報 -->
        <div style="background-color: rgb(104, 101, 101);">
            <img src="image/bell.png" alt="" style="width: 50px;">
        </div>
        <!-- 投稿内容 -->
        <div style="display: block;">
            <div style="background-color: rgb(109, 65, 65);">あああ</div>
            <div style="background-color: rgb(69, 65, 109);">いいい</div>
            <div style="background-color: rgb(65, 109, 88);">ううう</div>
            <!-- <div contenteditable="true" style="background-color: rgb(65, 109, 88);">ううう</div> -->
        </div>

    </div>

</body>

スクリーンショット 2020-09-19 23.45.22.png

block要素を上下左中央に配置

.outer{
  position: relative;
}
.inner{
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translateY(-50%) translateX(-50%);
  -webkit- transform: translateY(-50%) translateX(-50%);
}

引用:CSSで要素を上下や左右から中央寄せする7つの方法

フォーカスインとフォーカスアウト

addEventListenerでコントロールするものいいが特定のタグであれば
onfocusonblur属性がいい仕事をしてくれる

属性 説明
onfocus フォーカスしたとき
onblur フォーカスアウトしたとき
<input placeholder="キーワード検索" type="text" onblur="focusOut(this)" onfocus="focus(this)">

thisでその要素自体を取得できる

気になったcssプロパティなど

flex-basis

コンテンツボックスの寸法を定義

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

画面でエフェクトの実現

webシステムを開発するとき、画面上でどんな動作がありますか、どんなエフェクトがありますか。

課題あるいは結果まずを提出して、それに対して、どう対処すれば解決できるかを考えします。このようなプロセスにより、文章を作りましょう。

以下の動作があると思います。

・プールダウンリストでクリックすると、中の選択肢が表示された(HTML)
・ダイアログのポップアップ(Bootstrap)
・前入力ボックスを入力すると、後入力ボックスが自動入力された(jquery)
・日付ダイアログのポップアップ(jquery.date)
・「検索」ボタンをクリックすると、指定されたエリアで結果を表示する
(jquery(ajax))
・「更新」ボタンをクリックすると、次の画面に遷移する(HTML)

上記の動作あるいはエフェクトを次の文章で説明します。

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

HTML/CSS レスポンシブ対応 個人的まとめ

概要

フロントトエンドの学習記録。
レスポンシブなレイアウトをCSSで実現する方法。
キーワードは以下の通り。

  • ビューポート
  • メディアクエリ
  • widthのパーセント指定
  • flex-box

ビューポート

htmlのheader内に、metaタグとして埋め込みを行う。
モバイルデバイス(スマートフォン・タブレットなど)用の設定で、レスポンシブデザインを適応する際に必要な端末情報を取得して埋め込む。
主に、後述するメディアクエリの判定基準に使用される。

<meta name="viewport" content="width=device-width, initial-scale=1.0">

device-width: モバイル端末のCSSピクセル(コーディンする際の画面横幅)
※解像度には、CSSピクセル(HTMLコーディングで気にする) と デバイスピクスセル(画像で気にする)がある
例) iPhone X : CSSピクセル => 375 x 812 / デバイスピクセル => 1,125 x 2,436

initial-scale: ベージ表示時の初期拡大倍率
適切に、メディアクエリを用いてレスポンシブ対応する場合は1.0指定で概ね問題ない。
複数デバイスへの対応を力技で拡縮率だけで乗り切ろうとすると、1.0をjsでゴニョゴニョする必要もある、

なお、ビューポートは、CSS以外では、JavaScriptのwindow.innerWidth・innerHeightにも影響がある。

メディアクエリ

Media Queries

デバイスの画面サイズ別に、適応するCSSを変える(上書きする)などの場合に使用する、
@mediaで始まり、そのあとに、max-width か min-width でCSSの適応の境界値を設定する。
境界値となる値は、 ブレークポイント と呼ぶ。
ブレークポイント前にメディタイプを挟むこともできる。
@media screen | tv | printer みたいな感じ。省略するとallになる。

なお、ブレークポイントの指定はどちらも境界値を含む。

‘min-’ or ‘max-’ prefixes to express "greater or equal to" and "smaller or equal to" 
https://www.w3.org/TR/css3-mediaqueries/#media1

以下に使用例を示す

@media (max-width: 1000px) {
/* 最大画面サイズが1000px "まで(x <= 1000)" 適応したいCSS定義 */
}

@media (min-width: 1000px) {
/* 最大画面サイズが1000px "から(x >= 1000)" 適応したいCSS定義 */ 
}

@media (min-width: 1000px) and (max-width: 1500px)  {
/* 最大画面サイズが1000〜1500px まで適応したいCSS定義 */
}

実践的な使い方
実践的には、 スマホ / タブレット / デスクトップPC の3つでブレークポイントを設定するケースが多い。
ブレークポイントを沢山設定し過ぎてもメンテナンスしにくいので、これくらいがちょうど良い。
どの値をブレークポイントとするかだが、昨今のスマホ画面サイズの大型化の流れなどで、変化してきているようだ。
その時の、有識者がまとめている定番の値を、ググって適応するのが良さそう。
雰囲気的には 400-600 | 800-1024 に境界値を設けているところが多そうだ。

一応、現時点での、個人的な設定を記載する。
デバイスが徐々に大型化(多様化)する流れを感じつつ、少し大きめなところで線を引いてみた。

3つに分けるケース
モバイルファーストで作成しつつタブレットやデスクトップPCにも、きっちり出し分け対応したい場合。

/* スマホ用 (<= 640px) */
@media (min-width: 640px) {
}

/* タブレット用 ( <= 1024) */
@media (min-width: 1024px) {
}

2つに分けるケース
デスクトップPCの画面をメインとして、タブレットやスマホなども最低限見える程度に対応したい場合。
※ iPad Pro11の縦使用をPCとみなしたい場合は834を境界値に。

/* スマホ/タブレット用 (<= 900px) */
@media (min-width: 900px) {
}

widthのパーセント指定

メディアクエリを使って、それぞれのブレークポイント別に、コンテンツの表示件数を変える場合には、
widthをパーセント指定する。

以下に想定するケースを書く
・スマホ表示時には1つだけ newsコンテンツ を出す。
・タブレット表示時には2つ newsコンテンツ を横並びで出す。
・デスクトップ表示時には4つ newsコンテンツ を横並びで出す。

これをこうする。

.news {
  width: 25%;
}

/* スマホ用 (<= 640px) */
@media (min-width: 640px) {
  .news {
    width: 100%;
  }
}

/* タブレット用 ( <= 1024) */
@media (min-width: 1024px) {
  .news {
    width: 50%;
  }
}

box-sizing: border-box

調整をかけたいブロック要素に対して、paddingが設定された場合、
単純にパーセント指定だけだと厳しい局面がある。
なぜなら、paddingに対しては、パーセント計算が適応されないからである。
なので、画面サイズの小さい端末でみたときに、相対的に余白が大きく適応されて違和感が出てしまう。
また、floatで横並びレイアウトを組んでいた場合サイズが収まりきらなくて、箱落ちしてしまう。
これを防ぐために、 border-box 指定を使う。

* {
  box-sizing: border-box;
}

これにより、パーセント計算の時に、 余白も含めて(borderまで含めて) パーセント計算対象にしてくれるので、表示がいい感じに整う。
border-boxを使用しない場合は、旧来のcss内で、padding値を計算する方法もある。
なお、border-boxはmarginには適応されないので注意。

flex-box

フレキシブルにレイアウトを組むために入った仕組み。
レスポンシブデザイン向けの対応でよく用いられる。

使い方

  • 親要素に display:flex (子要素がインライン要素の場合はinline-flex)と並べる指向性を宣言する

    • flex-direction:並べる方向 => [row| column]-[ | reverse] で4パターンから指定
    • flex-wrap:折り返しルール => [nowrap | wrap | wrap-reverse]から指定
    • flex-flow: direction/wrap記述のショートハンド => flex-flow: row wrap;
  • 親要素にflexの全体での細かい動作適応設定を行う。(整列方法)

    • justify-content: 水平の整列ルール => [flex-start| flex-end | center | space-between | space-around]
    • align-items: 垂直の単一行整列ルール => [stretch | flex-start | flex-end | center | baseline]
    • align-content: 垂直の複数行整列ルール(wrap時) => justify-contentと同じ指定
  • 子要素で、要素個々の適応設定を行う。

    • order: 順番を制御 => あくまでも見た目だけ(DOMの並びは変わらないのでjsで要素抜くときは注意)
    • flex-grow: 相対的な拡大率 => 他要素を1で、特定要素だけ2にすると表示上幅2倍の比率で表示されるになる
    • flex-shrink: 相対的な縮小率 => growの逆
    • flex-basis: 子要素のベースの幅 => [auto | 200px | 25%] などで指定。
    • flex: grow/shurink/basisのショートハンド => flex: 0 1 30%;
    • align-self: (親要素をからの)子要素の垂直表示揃え => [auto | stretch | flex-start | flex-end | center | baseline ]

具体例
parante: .container
child: .item x N個
みたいな要素で構成されるマークアップ構造があったとする。

.container {
  display: flex;
  /** 子要素をrow/columnで並べ、reverseで順方向か逆方向かを指定。初期値はrow **/
  flex-direction: row-reverse;
}

.item1 { flex-grow: 2; }
.item2 { flex-grow: 3; }
.item3 { flex-grow: 1; }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

HTML/CSS 個人的まとめ

概要

レスポンシブなレイアウトをCSSで実現する方法。
キーワード。

  • ビューポート
  • メディアクエリ
  • widthのパーセント指定

ビューポート

htmlのheader内に、metaタグとして埋め込みを行う。
モバイルデバイス(スマートフォン・タブレットなど)用の設定で、レスポンシブデザインを適応する際に必要な端末情報を取得して埋め込む。
主に、後述するメディアクエリの判定基準に使用される。

<meta name="viewport" content="width=device-width, initial-scale=1.0">

device-width: モバイル端末のCSSピクセル(コーディンする際の画面横幅)
※解像度には、CSSピクセル(HTMLコーディングで気にする) と デバイスピクスセル(画像で気にする)がある
例) iPhone X : CSSピクセル => 375 x 812 / デバイスピクセル => 1,125 x 2,436

initial-scale: ベージ表示時の初期拡大倍率
適切に、メディアクエリを用いてレスポンシブ対応する場合は1.0指定で概ね問題ない。
複数デバイスへの対応を力技で拡縮率だけで乗り切ろうとすると、1.0をjsでゴニョゴニョする必要もある、

なお、ビューポートは、CSS以外では、JavaScriptのwindow.innerWidth・innerHeightにも影響がある。

メディアクエリ

Media Queries

デバイスの画面サイズ別に、適応するCSSを変える(上書きする)などの場合に使用する、
@mediaで始まり、そのあとに、max-width か min-width でCSSの適応の境界値を設定する。
境界値となる値は、 ブレークポイント と呼ぶ。
ブレークポイント前にメディタイプを挟むこともできる。
@media screen | tv | printer みたいな感じ。省略するとallになる。

なお、ブレークポイントの指定はどちらも境界値を含む。

‘min-’ or ‘max-’ prefixes to express "greater or equal to" and "smaller or equal to" 
https://www.w3.org/TR/css3-mediaqueries/#media1

以下に使用例を示す

@media (max-width: 1000px) {
/* 最大画面サイズが1000px "まで(x <= 1000)" 適応したいCSS定義 */
}

@media (min-width: 1000px) {
/* 最大画面サイズが1000px "から(x >= 1000)" 適応したいCSS定義 */ 
}

@media (min-width: 1000px) and (max-width: 1500px)  {
/* 最大画面サイズが1000〜1500px まで適応したいCSS定義 */
}

実践的な使い方
実践的には、 スマホ / タブレット / デスクトップPC の3つでブレークポイントを設定するケースが多い。
ブレークポイントを沢山設定し過ぎてもメンテナンスしにくいので、これくらいがちょうど良い。
どの値をブレークポイントとするかだが、昨今のスマホ画面サイズの大型化の流れなどで、変化してきているようだ。
その時の、有識者がまとめている定番の値を、ググって適応するのが良さそう。
雰囲気的には 400-600 | 800-1024 に境界値を設けているところが多そうだ。

一応、現時点での、個人的な設定を記載する。
デバイスが徐々に大型化(多様化)する流れを感じつつ、少し大きめなところで線を引いてみた。

3つに分けるケース
モバイルファーストで作成しつつタブレットやデスクトップPCにも、きっちり出し分け対応したい場合。

/* スマホ用 (<= 640px) */
@media (min-width: 640px) {
}

/* タブレット用 ( <= 1024) */
@media (min-width: 1024px) {
}

2つに分けるケース
デスクトップPCの画面をメインとして、タブレットやスマホなども最低限見える程度に対応したい場合。
※ iPad Pro11の縦使用をPCとみなしたい場合は834を境界値に。

/* スマホ/タブレット用 (<= 900px) */
@media (min-width: 900px) {
}

widthのパーセント指定

メディアクエリを使って、それぞれのブレークポイント別に、コンテンツの表示件数を変える場合には、
widthをパーセント指定する。

以下に想定するケースを書く
・スマホ表示時には1つだけ newsコンテンツ を出す。
・タブレット表示時には2つ newsコンテンツ を横並びで出す。
・デスクトップ表示時には4つ newsコンテンツ を横並びで出す。

これをこうする。

.news {
  width: 25%;
}

/* スマホ用 (<= 640px) */
@media (min-width: 640px) {
  .news {
    width: 100%;
  }
}

/* タブレット用 ( <= 1024) */
@media (min-width: 1024px) {
  .news {
    width: 50%;
  }
}

box-sizing: border-box

調整をかけたいブロック要素に対して、paddingが設定された場合、
単純にパーセント指定だけだと厳しい局面がある。
なぜなら、paddingに対しては、パーセント計算が適応されないからである。
なので、画面サイズの小さい端末でみたときに、相対的に余白が大きく適応されて違和感が出てしまう。
また、floatで横並びレイアウトを組んでいた場合サイズが収まりきらなくて、箱落ちしてしまう。
これを防ぐために、 border-box 指定を使う。

* {
  box-sizing: border-box;
}

これにより、パーセント計算の時に、 余白も含めて(borderまで含めて) パーセント計算対象にしてくれるので、表示がいい感じに整う。
border-boxを使用しない場合は、旧来のcss内で、padding値を計算する方法もある。
なお、border-boxはmarginには適応されないので注意。

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

初学者チームがWebサービス開発を始めて4週間で出来たことと出来なかったこと

2週間前にアップした初学者チームがWebサービス開発を始めて最初の2週間でやったことを振り返り、そして次を考えるという記事から、また2週間経ったので、進捗したのかまとめたいと思います。

「初心者だけど何かモノを作ってみたい!」という方の参考になれば幸いです。

目標達成度

2週間前掲げていた目標と、実際できたことを整理します。2週間前の記事ではこんなことを言っていました。

というわけで、次なる目標ですが、深い意味は特になく、なんとなくまた2週間先(9/21月曜)にチェックポイントを置きたいと思います。
目標は、9/21Monまでに人にURLをばら撒けるレベルのサイトにしたい!です。
具体的には、下記8点の要件を満たしたいです。

その8点と結果がこちら。

2週間前掲げた目標 達成度 10点満点 コメント
Heroku上でアプリが機能する 8 2週間前はHerokuにデプロイしたてでエラーもしばしば。今は表示は安定、基本動作も問題なし。
ページ内のリンクなど、ページがきちんと遷移する 5 まだ作れてないページがあったり、戻るボタン、キャンセルボタンなどが欠けています。
シンプルだが見るに耐えうるデザインであり全ページに統一感がある 3 フロントチームは頑張って作ってくれたのですが、サーバーサイドがそこまで手が回りませんでした。
投稿が新着順で表示でき、1ページの投稿は10件まで、それ以上は別ページとする 9 今ではペジネーションという専門用語も覚えました!でもデザイン配置のカスタマイズができてないので-1。
投稿がキーワード検索できる 0 全く手をつけてませんでした涙
アカウント登録した人がログイン、ログアウト、投稿、削除、編集できる 9 Laravelデフォルトのユーザ認証機能も利用しつつ、自分らのデザインを当て込むこともできてきました。
ゲストでも投稿できる 10 できました。
Contactページから問い合わせすると運営にメールが届く 5 今日滑り込みでメールを受信するまでの基礎機能の実装はできました。
総合得点 49/80 達成度61%

一方、ストレッチで掲げていた目標は綺麗に一切手を付けられませんでした(笑)

ストレッチ目標 達成度 10点満点 コメント
アカウントがアイコン画像を設定できる 0
投稿に画像を1枚添付できる 0
ミュージシャンごとの投稿を集約したページが生成できる 0
ライブハウスごとの投稿を集約したページが生成できる 0
いいね!ができる 0
利用規約のページがある 0
管理人が不適切な投稿を削除できる 0
総合得点 0/70 達成度0%

その他にこの2週間でできたこと箇条書き↓

感想3つ

(そういうのいいよって方は飛ばしてください)

初心者なのにモノを作ってみた感想

人それぞれ勉強方法はあるのかもしれませんが、基礎から教科書をやっていくという勉強方法よりも、実際にモノを作るというこの方法が僕には一番合っていると感じています。めっちゃくちゃ夢中になっています。ただし!1ページめくれば、次の手順を教えてくれるテキスト学習と違って、1歩進むごとに、何をしたらいいのかわからない、どう調べればいいかもわからない、手をつけたらエラーになるの繰り返しです。

上手くいかない時は、焦るし、焦って変なコマンド打ってしまったり、イライラしたり、精神的に安定しないこともありました。一般人は「プログラミングは超絶簡単」「1ヶ月でできるようになる」という有名人の言葉を鵜呑みにしない方がいいです。でも何かちょっと対策を講じてエラー文章が変わり、それで推理が進んで、原因を突き止めて、前に進んだ時の喜びと快感は尋常じゃなかったりします。

仲間という存在の貴重さ

まこなり社長さんが「プログラミング学習には仲間を作った方がいい」と仰っているのを聞いたことがありますが、僕も同意です。あれは単なるテックキャンプへ誘導する文言ではないと思います(笑)僕の理由は「行き詰まった時、話せる相手がいるのは超助かるから」です。相談する相手も初学者なので、答えがもらえるとは限らないのですが、一緒に調べてくれたり、解決のヒントを与えてくれたりします。自分の思考がクリアになって、話してるうちに閃くということも経験しました。そして何より精神的に落ち着けます。こういうことで悩んでるんだよねーというと、「わかるーー!」と言ってもらえるだけでなんか落ち着けるんですよね。女子の気持ちが少しわかりました(笑)

ただし、初心者のチーム開発はハッキリ言って楽ではありません。でもその大変さを差し引いてもプラスになっていると思います。

あと先輩やメンターの存在も結構でかいです。それらが揃ってるYouTube万屋エンジニアチャンネル超おすすめっす!

開発初期にはプログラミング以外の時間が結構かかる

最初の3週間はほぼコーディングをできなくて、アプリの設計を考えたり、開発環境を整えたりで手一杯でした。ようやく4週間めくらいからプログラムを書けるようになってきて、今楽しいです!

今後の開発

次の目標

期限:10/5月曜18:00
下記10点の実装を目指します。

  • 継承レイアウトで構築し、サイト全体にデザインの統一感がある
  • 投稿の文章が長い場合、全文表示/一部表示の切り替えができる
  • ドロップダウンメニュー
  • 投稿の際、チップスの効いたタグ付けができる
  • 投稿に付いたタグをクリックするとそのタグの投稿一覧を表示できる
  • 登録ユーザーの投稿一覧が表示できる
  • 検索機能
  • いいね機能の実装
  • Contactから運営側にメールが届き、問い合わせ内容がわかる
  • ユーザーがプロフ画像を設定できる

開発体制の変更

今まではフロントには一旦別環境でHTML/CSS作りに集中してもらっていたのですが、開発が進んできてそれでは大変になってきそうなので、フロントチームもLaravelを入れてもらってバックエンドと同じ環境下でフロントのブラッシュアップをしてもらうつもりです。

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

【初心者でもわかる】フォームの必須項目に(*)や(※)を付ける方法

どうも7noteです。必須項目と分かるように装飾を作る方法

今回はCSSで上付き文字を作ります。こんな感じです。

sample.png

作り方

index.html
<p>お名前<span></span></p>
style.css
span{
  color: red;             /* 文字色を赤にする */
  font-size: 0.5em;       /* 文字サイズを半分にする */
  vertical-align: super;  /* 上付き文字にする */
}

上付きにしたい文字を<span>で囲み、vertical-align: super;で上に配置します。
vertical-alignはインライン要素にしか効かないので、注意が必要です。

<sup>は使うな!

似たようなことができる要素として<sup>というものがありますが、<sup>はその文字がないと意味が変ってしまうものにしか使わないというのが推奨の使い方になります。

たとえば、H2Oとか、E=mc2とかその文字が上付き文字でなくなってしまうと意味が変ってしまいます。
このような文字の場合にのみ<sup>を使います。

まとめ

勉強しはじめの頃、vertical-alignを使っていると上手く効いたり効かなかったリするので、必ずその要素がインライン要素かどうかを確認するよう注意してください。

おそまつ!

~ Qiitaで毎日投稿中!! ~
【初心者向け】HTML・CSSのちょいテク詰め合わせ

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

この画像ファイルは正しいJavaScriptファイルです

以下はSebastian Stamm ( Twitter / GitHub / Webサイト )による記事、This Image Is Also a Valid Javascript Fileの日本語訳です。

01.gif

This Image Is Also a Valid Javascript File

画像は普通はバイナリファイルであり、Javascriptファイルは基本的にテキストファイルです。
いずれも固有のルールに従わなければなりません。
画像ファイルは、データをエンコードするための具体的なフォーマット形式が決まっています。
Javascriptファイルは、実行するためには特定の構文に従わなければなりません。
ところでふと気になりました。
Javascriptとして実行可能な有効な構文を持っている画像ファイルを作成することはできるでしょうか?

ここより先を見る前に、実験の結果を以下のサンドボックスで確認してみることをお勧めします。

https://codesandbox.io/s/executable-gif-8yq0j?file=/index.html

自身で画像をダウンロードして確認してみたければ、こちらからダウンロードできます。

https://executable-gif.glitch.me/image.gif

Choosing the Right Image Type

残念ながら画像には大量のバイナリデータが含まれているため、そのままJavascriptとして解釈しようとするとエラーになります。
そこで私がまず考えたのは、次のようなものでした。
以下のように、全ての画像データをコメントに入れたらよいのではないかということです。

/*ALL OF THE BINARY IMAGE DATA*/

これは有効なJavascriptファイルになります。
しかし、画像ファイルは特定のバイト列で開始される必要があります。
たとえばPNGファイルの先頭は常に89 50 4E 47 0D 0A 1A 0Aです。
最初が/*で始まっていたら、それは有効な画像ファイルではありません。

このヘッダを見ていて次のアイデアを思いつきました。
バイト列を変数名にして、バイナリは文字列として代入できればよいのではないかということです。

PNG=`ALL OF THE BINARY IMAGE DATA`;

バイナリデータには改行コードが含まれている可能性があり、改行コードの扱いはテンプレートリテラルのほうが優れているので、通常の"'ではなくテンプレートリテラルを用いることにしました。

残念ながら、ほとんどの画像ファイルはヘッダ部のバイト列に変数名として許されない文字を含んでいます。
しかし、それが可能な画像フォーマットをひとつ発見しました。
GIFです。
GIFファイルのヘッダは47 49 46 38 39 61で、これは文字列GIF89aをASCIIで綴ったものであり、すなわち完全に合法ということです。

Choosing the Right Image Dimensions

有効な変数名で始めることができる画像フォーマットを見つけたので、次はそこに等号=とバックティック`が必要です。
従って、ここではファイルの次の4バイトを3D 09 60 04とすることにします。

02.png

GIFフォーマットでは、ヘッダの次の4バイトは画像のサイズを表します。
この中に等号の3Dとバックティックの60を埋め込まなければなりません。
GIFはリトルエンディアンであるため、画像サイズにはそれぞれ2バイト目が大きな影響を与えます。
何万ピクセルもある画像にならないように、3D60は下位バイトに格納することにしました。

画像サイズの残ったバイトには、GIF89a= `という有効文字列を残すために空白文字を入れます。
最も画像幅を小さくできる有効な空白文字は水平タブ09であり、従って画像幅は3D 09、リトルエンディアンでは2365になります。
思ったよりは大きいですが、まだ妥当なサイズです。

画像の高さについては、ちょうどよいアスペクト比になりそうなものを選ぶことができます。
今回は04を選択したので、画像の高さは60 04、すなわち1120です。

Getting our own script in there

このJavascriptは今のところ何もしません。
グローバル変数GIF89aに文字列を代入しているだけです。
せっかくなので何か面白いことがおこるようにしたいですよね。
しかし、GIFファイル内のほとんどは画像をエンコードするためのデータなので、そこにJavascriptを入れようとすると、それは単に壊れた画像になってしまうでしょう。
ところで何故か、GIFフォーマットにはComment Extensionという機能が含まれています。
これはGIFデコーダで解釈されないメタデータを保存する場所であり、すなわちJavascriptのロジックを配置するのに最適な場所ということです。

Comment ExtensionはColor Tableのすぐ後ろに配置されています。
ここには任意の文字列を入れることができるため、変数GIF89aの文字列を閉じることも簡単です。
そこで任意のJavascriptを記述し、最後にコメント開始を入れることで、残りの画像の部分をJavascriptパーサに解釈されないようにします。

全体として、ファイルの中身は以下のようになります。

GIF89a= ` BINARY COLOR TABLE DATA ... COMMENT BLOCK:

`;alert("Javascript!");/*

REST OF THE IMAGE */

ただし、少しばかり制限があります。
Comment Extensionは複数のサブブロックで構成する必要があり、ブロックごとの最大サイズは255です。
そしてサブブロックの終わりには、次のサブブロックの長さを表すバイトを記述しなければなりません。
従って、大きなスクリプトを入れたい場合は、以下のように小さな部品に分解していく必要があります。

alert('Javascript');/*0x4A*/console.log('another subblock');/*0x1F*/...

コメント中に書かれているhexcodeは、次のサブブロックの長さを表しています。
これはJavascriptには不要ですが、GIFフォーマットとしては必要です。
従って、Javascriptの邪魔にならないようにJavascriptコメント中に書かなければなりません。
この問題を解決するため、スクリプトをチャンクに分けるための小さなスクリプトを書きました。

https://gist.github.com/SebastianStamm/c2433819cb9e2e5af84df0904aa43cb8

Cleaning up the Binary

基本的な構造が分かったので、次は画像データのバイナリがJavascript構文を壊さないようにする必要があります。
上で説明したように、ファイルには3つのセクションがあります。
ひとつめは変数GIF89aへの代入部分、ふたつめがJavascriptコード、最後は複数行コメントです。

まずは最初の変数代入部分を見てみましょう。

GIF89a= ` BINARY DATA `;

バイナリデータに`、もしくは${が入っていたら、そこでテンプレート文字列が終了したり無効な式が生成されてしまうため困ったことになります。
この修正はとても簡単です。
すなわち、バイナリを変更するだけです。
たとえばバイナリ中にASCIIコード60があったら、それを61、すなわち文字aに変更します。
ここはカラーパレットの定義部分のデータを変更することになるため、結果として一部のカラーコードが、たとえば#286048から#286148へと微妙に変化することになります。
しかし、この違いに誰かが気付く可能性は非常に低いでしょう。

Fighting the corruption

Javascriptコードの最後に、バイナリデータがJavascriptに影響しないようにコメント開始コードを書きました。

alert("Script done");/*BINARY IMAGE DATA ...

もし画像のバイナリに*/が含まれていたら、そこでコメントが終了してしまい、不正なJavascriptファイルになってしまいます。
従って、ここでも同じように2文字のうちどちらかを変更することで、コメントが終了しないようにすることにします。
しかし、ここは既にエンコードされた画像セクションであるので、単純に書き換えるだけではこのように画像が壊れてしまいます。

03.gif

極端な場合、画像が全く表示されなくなることすらありました。
しかしどの文字を変更するかを慎重に選択することで、画像の破損を最小限に抑えることができました。
幸いなことに、問題になるような*/はほんの少しだけでした。
最終的な画像は、Valid Javascript File文字列の下のほうなどに僅かな破損が見えますが、概ね満足のいく仕上がりになっています。

Ending the File

最後にしなければならない問題は、ファイルの最後にあります。
ファイルの末尾は00 3Bで終わらせなければならないため、最後までコメントで埋めることはできません。
ファイルの終わりであるため、バイナリデータの変更は目に見えるような画像の破損を起こしません。
従って、Javascriptが正常に動くように、コメントブロック終了後に1行コメントを追加しました。

/* BINARY DATA*/// 00 3B

Convincing the Browser to Execute an Image

ここまできて、ついにJavascriptとして有効な画像ファイルが完成しました。
しかし、最後にまたひとつ問題があります。
この画像をサーバにアップロードし、scriptタグで読み込もうとすると、次のようなエラーが発生する可能性があります。

Refused to execute script from 'http://localhost:8080/image.gif' because its MIME type ('image/gif') is not executable.

ブラウザは『こいつは画像ファイルだから実行なんてしないぞ』と拒否します。
大抵の場合、これは適切な動作と言えましょう。
しかし、私はこれを実行したいのです。
ということで、その解決策はそのファイルが画像であることをブラウザに伝えないことです。
そのためだけに、ヘッダ情報を送らず画像ファイルだけを返すサーバを作りました。

ヘッダのMIME typeがなければ、ブラウザはそのファイルの正体が何であるかわからず、コンテキストに即したものを実行します。
すなわち、<img>タグでは画像として表示し、<script>タグではJavascriptとして実行します。

But ... why?

なんのため?
それは、私がまだ解明できていないことです。

これを作るのは精神的によい挑戦でした。
もし、これが実際に役立つようなシナリオを思いついたら、ぜひ教えてください!

コメント欄

「面白かった!」
「?」
「こいつはクールだ……ぜ??」
「画像ファイルにトラッカーを仕込むとか?もうひとつ思いついたのは、単純にハッキングとか。」「画像に情報を仕込む技術は既にある。ステガノグラフィでぐぐれ。」
「セキュリティへの影響について考えてる。imgタグに入ってる画像をブラウザが勝手にJavaScriptとして実行したら最悪だ。」「imgタグはJavaSctiptと解釈されないから問題ない。」

感想

img srcscript srcどちらで読み込んでも正しく動作する、面白ファイルが出来上がりました。

手法自体はコメントにバイナリを埋め込むというもので、古来より幾度となく行われているものですが、今回はJavaScriptを動くようにしたうえで画像としても破綻無く仕上げているところが面白いですね。

この手法の実用的な例というとCutwailがありますが、これは別に正しい画像である必要はないからちょっと違うかな。

今はセキュリティ上できないようになっているはずですが、この手法を突き詰めたらいずれ、
・Webサイトにアクセスしたら、一枚の画像ファイルが落ちてきた
・画像の表示が終わったと思ったら何故かHTMLになっていた
みたいなこともできるかもしれませんね。

そんなことをやって何のメリットがあるのかさっぱりわかりませんが。

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

時間はないけれど、最短最速でプログラミングを身に付けたい!【基礎の基礎】

超ハイパースペシャル初心者の“ぴおにあ”でーす:v:

最初に投稿してから一か月以上たってしまった。はやっ。

さて、この約一か月間、会社員、かつ学生で時間が無いド素人の私が、最短最速独学でプログラミングを習得するために何をしていたか。

今日はそれをお教えしましょう(ΦωΦ)フフフ…

今までは本当にプログラミングのプの字もわかっていなかったので、Qiitaの書き方も全くわかりませんでした。

わろ先生に以下のページを教えていただいたものの、書いてあることは当然未知のことだらけ^^;

参照: Markdown記法 チートシート

「えっ、Qiitaって何かの言語を使って書くの?何の言語なの?私はまだHTMLの基礎の基礎しか知らないよ?」
そうなんです、言語とは何かさえよく分かっていなかった(いや今も分かっているわけではないけれど、汗)

わはは? そんな状態からは一歩脱出いたしました。というわけで、今回はこれを参考に少し見やすく書いてみましたよ。

基礎知識

HTML・CSS・Java Scriptの基礎知識

とにかく、MDN Web Docを熟読。
これは本当に良いです。超初心者におすすめ。ページは少し真面目でとっつきにくいんだけど、
無料でこんなに勉強できてしまうなんて少し感動すら覚えました。説明もなかなか丁寧。これを何度も読みました。

参照: MDN Web Doc

Qiitaやドットインストールを参考に

超初心者にドットインストールは優しい!

ドットインストールは3分以内の無料動画(2020年9月21日時点で436レッスン、6516本の動画がアップ)でプログラミングを学ぶ準備からガイドしてくれます。3分間なので要所を抑えたい時や調べものの時、このサイト内でキーワード検索するなどして使用し、大変便利です。

参照: ドットインストール 


Qiitaは知や経験が豊富に公開されている

少し新鮮な驚きなのですが、エンジニアさんたちの世界って共有が一つのキーになっているんですね。素敵な世界❤️
見知らぬ多くの人たちが、経験や知識を惜しみなく共有してくれている場がこんなにたくさんある(私が知っているのはQiita、Github、MDN、dotinstallくらいですが)ことに嬉しい驚きです。
これを存分に使いましょう。タグで検索し不明点を調べたりしています。

参照:  Qiita:わろ先生の「JavaScriptによる「順次」「繰り返し」「分岐」」ページ

動画学習

Udemy
 
動画学習にも挑戦!!! 以下参照ページのわろ先生おすすめコースの中から、ちょうどUdemyがセールをしていたので1500円のHTML・CSS・Java Script の初級コースを3つ購入。(プログラミングの勉強で初出費)
全部で9時間程度のコースをひとつまずは受講してみました。動画学習は実際にコードを書いてそれがどう表示されるのかを見ることができるので、記憶に残りやすいと感じます。
私の場合には時短のために、速度を1.5倍速に上げて聞きました。1.5倍でも講師の説明は十分聞き取れます。分からないところはメモして何度でも見る癖をつけるようにしたいです。

参照: 【保存版】Udemy 350コース突破記念 講師に全力 リスペクト企画 オススメ コース紹介【入門者向け】

Youtube

Blockchainの勉強
 
これは技術ではなく、概念を学ぶため。最初にyoshitechで事前資料として指定された以下の動画や同じIBM Japan channnel の「ブロックチェーンの仕組み」などを何度も見ています。

参照: ブロックチェーンとは何か? by IBM Japan channnel


私は目的がBlockchainを使ったエシカルな仕組み作りなので、HTML/CSS の凝った装飾テクニックなどは後回しにしてJavaScriptへ進んでいます。
今の時点で、平均1日50分間程度は学習に当てられているようですが、学校のテスト期間やレポート次第では増減するでしょう。

状況を見ながら、学習環境なども見直してゆきたいです。

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

font-awesomeとbootstrap(4.5.1)の導入

各種バージョン

Ruby: 2.6.5
Rails: 6.0.0
Bootstrap: 4.5.1
Font-Awesome(Free): 5.7.2

app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>boot_and_fontawesome_app</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
    <link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/3.18.1/build/cssreset/cssreset-min.css">
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">
    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
    <script src="https://kit.fontawesome.com/d9fcea61b7.js" crossorigin="anonymous"></script>
  </head>

  <body>
    <%= yield %>
    <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script>
  </body>
</html>

上記を貼るだけ
以下はサンプルコード。

app/views/index.html.erb
<div class="container">
  <div class="jumbotron m-3">
    <h1 class="display-4">Hello, Bootstrap!</h1>
    <hr class="my-4">
    <p>This is the template of Ruby on Rails using Bootstrap.</p>
    <button type="button" class="btn btn-primary btn-lg" data-toggle="modal" data-target="#exampleModal">
      <i class="far fa-window-maximize"></i>
      Click here
    </button>
  </div>
  <!-- Modal -->
  <div class="modal fade" id="exampleModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
    <div class="modal-dialog" role="document">
      <div class="modal-content">
        <div class="modal-header">
          <h5 class="modal-title" id="exampleModalLabel">Modal title</h5>
          <button type="button" class="close" data-dismiss="modal" aria-label="Close">
            <span aria-hidden="true">&times;</span>
          </button>
        </div>
        <div class="modal-body">
          Hello!
        </div>
        <div class="modal-footer">
          <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
        </div>
      </div>
    </div>
  </div>
</div>

すると以下のようなページを表示できます。
https://gyazo.com/2c476830157ce55b86000591254edc33
ブートストラップとfont-awesomeが適用できていることがわかります。

参考 https://getbootstrap.jp/docs/4.5/getting-started/introduction/

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

(´ω`) ... VueのTable {width,align}を一括指定するディレクティブつくりました

前回、DatGuiをBackEnd APIを検証するための登録画面に利用している。って記事を記載した。がだれからも良いねはなくLGTMも来なくて内心ビビってる。マジか。と思った。というかツイッターのフォロワーは未だ増えないままだ。この現状を見てInputMaskをぶっこんだ話なんかも展開する予定だったがやめた。そしてQiitaフォントサイズ大きいなってことで自分でブログを作ろうと思った。プロトタイプはあるし順調だ。だからその話もメモ代わりにQiitaで展開していくつもりだ。そこで動作するサンプルなんかも公開しよう。その間の記事はQiitaにメモ代わりで投稿することにした。

1.前提

趣味で何かを開発していたら、テーブルの列が後から増えてくる(登録する項目なんか)ってのはよくある話だ。少なくとも自分の間じゃそうだ。最初のプロトタイプを作るときは名前なんて姓だけで十分だしあとIDとPASSの3項目でテーブル作って社員登録機能を作る。みたいな最小限の構成で組み始めている。できた次に社員名いっとくか。メールアドレスいっとくか。みたいに項目を増やしていく。(機能が出来たら次のデータを作る。棚田だ)
image.png

2.問題

ここで起きてくる問題はCSSだ。今や<tag>の中にstyleをぶっこむなんて真似も全くしてこなかった俺だが、このような作り方の前では後から項目が増えるからID指定やCLASS指定でやろうと面倒くさくなってくる。nth-child(n)で最初は指定していたが列間の途中に項目を増やす状況になると、配列途中に値を挿入するみたいに以降の全番号を手で振りなおすという愚かさだ。面倒くさい(姓と名は左寄せのほうがいいな)

sample.css
table tr td:nth-child(1) { width:6em; text-align:center; }
table tr td.employee-name { width:6wm; text-align:center; }
table tr td#employee-name { width:6em; text-align:center; }
table tr td[data=employee_name] { width:6em; text-align:center; }

3.結局

タグの中に書き込んだ方が一番手っ取り早い。という結論に至った。周りはどうかな?どうやってるか知りたい。だが、なぜ毎回widthやtext-alignを打ち込まなくちゃならないんだ?面倒くさい。APIを検証するためのこの画面ではテーブルの項目は永遠と増え続ける。テーブル自体もだ。そのたびに書くのは面倒だ。その間の時間を省いてチロルチョコを食べた方がいい。健康的な考えだ。

sample.html
 <table>
   <tr>
     <td style='width:6em;text-align:center;'></td>
  </tr>
</table>

4.ということでディレクティブを作った

前置きが長かった。要はセルのtdのwidthとalignを{attribute:value}をvalueの一括指定でDOM作ってくれたら便利だなという話だ。ソースはこれ。

directive.vue
   Vue.directive('style',function(el,binding,vnode){
     vnode.context.$nextTick(function(){
       var arr = binding.value.split(',') 
       switch(binding.arg){
         case 'align':
           _.each(el.children,function(el,i){
             var align = arr[i] == 'l' ? 'left' : arr[i] == 'r' ? 'right' : 'center'
             el.classList.add(align)
           })
           break;
         case 'width':
           _.each(el.children,function(el,i){
             el.style.width = (arr[i] == undefined) ? 'auto' : arr[i] + 'em'
           })
           break;  
       }
     })
   })

5.使い方

ディレクティブをかますだけ。<tr>に。列の幅指定はヘッダだけで良く配置指定はボディに組み込んでいるが、確かにv-style:alignは効率的じゃないかもしれない。全セルに指定することになるからループも多い。ここだけCSSにした方がいいかも。どうやるのがベストだと思うか知りたい。ダブルクォートの間にシングルクォートを挟むのはこれ自体を文字列として評価してもらうためだ。

それからこのコードはLODASHに依存している。_.each()のとこだけだ。

sample.html
 <table>
   <thead>
     <tr v-style:width="'7,7,7,5,5,5,5,5,5,5,5,5,5,auto'">
    </tr>
  </thead>
   <tbody>
     <tr v-for='v,k,n in list' v-style:align="'c,l,l,c,c,c,c,c,c,c,c,c,c,c'">
    </tr>
  </tbody>
</table>

image.png

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

(´ω`) Let's Vue Programming, 登録画面にDatGui, Enter送りを添えて

1.DatGuiってこんなん

こんにちは。職業グラマです。最近ツイッター始めたのをきっかけにQiitaもやってみるか!ってことでトライしてます。ご紹介するのはDatGui。言わずもがなJavaScriptのパラメータ調整用画面をさくっと作れる便利ライブラリです。ゲーム画面やThree.JSを利用した画面なんかでよく利用されています。このFormのパラメータを調整するとJavaScriptのオブジェクト側にも値の変更を行ってくれるのでリアルタイムなゲームの制御また調整ができます。
image.png

2.DatGuiを登録画面に使ってみました

今回私は、このDatGuiをバックエンドのAPIを検証するためのフォームとして利用しました。社員の登録とか削除とか上書するための画面に使います。サクッと作れてコンパクト。だから項目が増えてもBootStrapやSemantic-Uiに比べて見晴らしがよく手間を減らせます。空いた時間にカップラーメン食えるぐらいには良い感じです。
screencapture-127-0-0-1-8888-20200908-dev-2020-09-21-00_42_33.png

3.DatGuiを登録画面にエンター送りを組み込みました

エンター送りってExcelのセルにEnterすると次セルに移動するあれです。伝票とか一日に何度も入力するフォームとかってTabで次の入力インプットに飛ばしたりするの面倒だから項目入力後のEnter Key押されたタイミングで自動的に次インプットにフォーカスを移動する。これがエンター送りです。

社員姓打ち込んでエンターしたら自動的に社員名の入力インプットにフォーカスが移る。って感じです。それを実現するためのコードがこちら。Vueのディレクティブとして構築してます。DatGuiにはtype=textのコントロールが公式にはありませんが私は個人的に組み込みました。動くサンプルとかどっかにあげようと考えてます。

visibleつけているのは、これが入力されているときは次項目を表示しない。といった場面でも正しく動作させるためです。
次INPUTがv-show='false'(display:none;)のときにそれを含まずにキチンと2個目のコントロールにジャンプしてくれます。

vue.directive
   Vue.directive('focus-on-enter-next',function(el,binding,vnode){

     var namespace = binding.arg
     var callback = (binding['value'] !== undefined) ? binding.value : function(){}
     var inputs = $([
       'input[type=password]:visible',
       'input[type=checkbox]:visible',
       'input[type=text]:visible',
       'textarea:visible',
       'select:visible'
     ].join(','),el)

     inputs.off([
       'focus.' + namespace,
       'keydown.' + namespace,
       'focusout.' + namespace
     ].join(' '),el)

     inputs.on('focus.' + namespace,function(ev){
       $(el).closest('.cr').addClass('focus')
     })
     inputs.on('focusout.' + namespace,function(ev){
       $(el).closest('.cr').removeClass('focus')
     })
     inputs.on('keydown.' + namespace,function(ev){
       if(ev.keyCode == 13){
         var i = inputs.index(this)
         var n = (i+1)
         if (n < inputs.length){
           inputs.eq(n).focus()
         }else{
           callback()
         }
         switch(inputs.eq(n).prop('localName')){
           case 'textarea':
           case 'input':
             return false;
         }
       }
     })
   })

4.エンター送りの呼び出し側コード

v-focus-on-enter-next:event_namespace='function_complete'といった形で利用できます。event_namespaceは1画面中にある複数のDatGui毎にエンター送りを適用するためです。function_completeは最後の入力INPUTが完了したタイミングで呼び出されます。ここでは備考です。入力してENTERしたタイミングで呼び出されます。なので入力後に社員登録ボタンを押してAPIを呼び出さなくてもfunction_completeの中に記述しておけば備考までの入力が完了したタイミングで自動的に登録APIを処理できます。楽するためです。

example.html
  <dat-gui v-focus-on-enter-next:employee='complete'>
    <dat-value v-model.number='params.社員コード' label='コード' :min='0' :max='9999' :step='1' :empty_n='0'></dat-value>
    <dat-image v-model='params.社員アバター' label='画像'></dat-image>
    <dat-value v-model='params.社員姓' label='苗字'></dat-value>
    <dat-value v-model='params.社員名' label='名前'></dat-value>
    <dat-value v-model='params.社員姓かな' label='苗字-かな'></dat-value>
    <dat-value v-model='params.社員名かな' label='名前-かな'></dat-value>
    <dat-value v-model='params.社員性別' label='性別' :accepted-values="['男性','女性']"></dat-value>
    <dat-value v-model='params.社員パスワード' label='パスワード'></dat-value>
    <dat-value v-model='params.社員担当部署' label='担当部署' :accepted-values="['管理','営業','開発']"></dat-value>
    <dat-value v-model='params.社員入社日' label='入社日'></dat-value>
    <dat-value v-model='params.社員管理権' label='管理権'></dat-value>
    <dat-text v-model='params.社員備考' label='備考'></dat-text>
 </dat-gui>

5.DatGui Vue Plugin忘れてました

lq111lq/vue-dat-guiを1ファイル化してTextコントローラを入れた版です。その他empty_nとかwideとかplaceholderとかreadonlyとかpasswordとか公式にはないプロパティは私個人が組み込んだものになります。

wide属性をdat-guiタグで有効(true)にすればDatGui左側のラベルが消えて幅広く入力できます。
ラベル名はそのままInputのplaceholderに適用されます。
password属性をdat-valueタグで有効にすれば入力内容が****として隠されます。
readonly属性は編集不可にするだけです。社員に自動付与されたIDの表示なんかに使います。
empty_nはスライダーボックスでempty_nのnの値の時、ボックス内を空文字にしてくれます。自分で社員IDを打ち込むときに使います。

あと、DatGuiのプルリクエストにあったイメージコントローラとか、スライダーボックス上でマウスホイール又は上下キーでカウントアップ/カウントダウンを組み込んでます。これで登録も楽になるかなーと考えてました。

vue.directive
(function(root){

  var plugin={
    install:function(Vue,options){

      Vue.component('DatGui',{
        template:"\
          <div>\
            <div ref='container'>\
              <slot></slot>\
           </div>\
         </div>\
        ",
        name:'DatGui',
        props: {
          wide: {
            type: Boolean,
            default: false
          }
        },
        watch: {
          wide:function(value) {
            this.$_gui && this.$_gui.wide(value)
          }
        },
        data:function(){
          return {
            context: {}
          }
        },
        provide:function() {
          return {
            context: this.context
          }
        },
        created:function() {
          this.$_gui = new dat.GUI({ autoPlace: false })
        },
        mounted:function() {
          this.$refs.container.appendChild(this.$_gui.domElement)
          this.$_gui.wide(this.wide)
        },
        beforeDestroy:function() {
          this.$_gui && this.$_gui.destroy()
        }
      })
      Vue.component('DatFolder',{
        template:"\
          <span>\
            <slot>\
           </slot>\
         </span>\
        ",
        props: {
          name: {
            type: String,
            default: 'folder'
          },
          wide: {
            type: Boolean,
            default: false
          }
        },
        watch: {
          wide:function(value) {
            this.$_gui && this.$_gui.wide(value)
          }
        },
        created:function() {
          this.$_gui = this.$parent.$_gui.addFolder(this.name)
        },
        mounted:function() {
          this.$_gui.wide(this.wide)
        }
      })
      Vue.component('DatValue',{
        template:"\
          <span>\
         </span>\
        ",
        name: 'DatValue',
        props: {
          value: {
            type: [Number, String, Boolean, undefined],
            default: undefined
          },
          label: {
            type: String
          },
          max: {
            type: Number,
            default: null
          },
          min: {
            type: Number,
            default: null
          },
          step: {
            type: Number,
            default: null
          },
          empty_n: {
            type: Number,
            default: null
          },
          acceptedValues: {
            type: [Object, Array],
            default:function() {
              return null
            }
          },
          placeholder: {
            type: String,
            default: ''
          },
          readonly: {
            type: Boolean,
            default: false
          },
          password: {
            type: Boolean,
            default: false
          }
        },
        inject: ['context'],
        computed: {
          valueInner:function() {
            return this.context && this.context[this.label]
          }
        },
        watch: {
          value:function(value) {
            this.$_controller && this.$_controller.setValue(value)
          },
          valueInner:function(newVal) {
            switch(true){
              case this.$_controller instanceof dat.controllers.NumberController:
              case this.$_controller instanceof dat.controllers.NumberControllerBox:
                this.$emit('input', newVal <= this.empty_n ? '' : newVal)
                break;
              default:
                this.$emit('input', newVal)
            }
          },
          label:function(value) {
            this.$_controller && this.$_controller.name(value)
          },
          max:function(value) {
            this.$_controller && this.$_controller.max(value)
            this.$_controller.updateDisplay()
          },
          min:function(value) {
            this.$_controller && this.$_controller.min(value)
            this.$_controller.updateDisplay()
          },
          step:function(value) {
            this.$_controller && this.$_controller.step(value)
            this.$_controller.updateDisplay()
          },
          empty_n:function(value){
            this.$_controller && this.$_controller.empty_n(value)
            this.$_controller.updateDisplay() 
          },
          acceptedValues:function(list) {
            var html = ''
            var type = Object.prototype.toString.call(list).slice(8,-1).toLowerCase()

            switch(type){
              case 'array':
                list.forEach(function(v){
                  html += '<option value=' + v + '>' + v + '</option>';
                })
                break;
              case 'object':
                Object.keys(list).forEach(function(k){
                  html += '<option value=' + list[k] + '>' + k + '</option>';
                })
                break;
            }

            if(this.$_controller){
              this.$_controller.domElement.children[0].innerHTML = html
            }
          },
          placeholder:function(value) {
            this.$_controller && this.$_controller.placeholder(value)
          },
          readonly:function(value) {
            this.$_controller && this.$_controller.readonly(value)
          },
          password:function(value) {
            this.$_controller && this.$_controller.password(value)
          }
        },
        created:function() {
          var self = this

          if (this.value === undefined) {
            this.$set(this.context, this.label, function() {
              self.$emit('click')
            })
            this.$parent.$_gui.add(this.context, this.label)
            return
          }

          this.$set(this.context, this.label, this.value)
          if (this.acceptedValues) {
            this.$_controller = this.$parent.$_gui.add(this.context, this.label, this.acceptedValues)
            this.$_controller.onFinishChange(function(obj){
              var has_selected_option = this.__select.selectedOptions.length > 0
              if(has_selected_option){
                self.$emit('change',this.__select.selectedOptions[0].innerText)
              }
            })
          } else if (this.min != null && this.max != null && this.step != null) {
            if (this.empty_n != null){
              this.$_controller = this.$parent.$_gui.add(this.context, this.label, this.min, this.max, this.step, this.empty_n)
            } else {
              this.$_controller = this.$parent.$_gui.add(this.context, this.label, this.min, this.max, this.step)
            }

            this.$_controller.onChange(function(value){
              self.$emit('change',value)
            })
          } else {
            this.$_controller = this.$parent.$_gui.add(this.context, this.label)
            this.$_controller.placeholder(this.placeholder)
            this.$_controller.readonly(this.readonly)
            this.$_controller.password(this.password)
          }
        }
      })
      Vue.component('DatColor',{
        template:"\
          <span>\
         </span>\
        ",
        name: 'DatColor',
        props: {
          value: {
            type: [String, Array, Object]
          },
          label: {
            type: String
          }
        },
        inject: ['context'],
        computed: {
          valueInner:function() {
            return this.context && this.context[this.label]
          }
        },
        watch: {
          value:function(value) {
            this.$_controller && this.$_controller.setValue(value)
          },
          valueInner:function(newVal) {
            this.$emit('input', newVal)
          }
        },
        created:function() {
          this.$set(this.context, this.label, this.value)
          this.$_controller = this.$parent.$_gui.addColor(this.context, this.label)
        }
      })
      Vue.component('DatText',{
        template:"\
          <span>\
         </span>\
        ",
        name: 'DatText',
        props: {
          value: {
            type: String
          },
          label: {
            type: String
          },
          placeholder: {
            type: String,
            default: ''
          },
          readonly: {
            type: Boolean,
            default: false
          }
        },
        inject: ['context'],
        computed: {
          valueInner:function() {
            return this.context && this.context[this.label]
          }
        },
        watch: {
          value:function(value) {
            this.$_controller && this.$_controller.setValue(value)
          },
          valueInner:function(newVal) {
            this.$emit('input', newVal)
          },
          label:function(value) {
            this.$_controller && this.$_controller.name(value)
          },
          placeholder:function(value) {
            this.$_controller.placeholder(value)
          },
          readonly:function(value) {
            this.$_controller.readonly(value)
          }
        },
        created:function() {
          this.$set(this.context, this.label, this.value)
          this.$_controller = this.$parent.$_gui.addText(this.context, this.label)
          this.$_controller.placeholder(this.placeholder)
          this.$_controller.readonly(this.readonly)
        }
      })
      Vue.component('DatImage',{
        template:"\
          <span>\
         </span>\
        ",
        name: 'DatImage',
        props: {
          value: {
            type: String
          },
          label: {
            type: String
          },
          placeholder: {
            type: String,
            default: ''
          }
        },
        inject: ['context'],
        computed: {
          valueInner:function() {
            return this.context && this.context[this.label]
          }
        },
        watch: {
          value:function(value) {
            this.$_controller && this.$_controller.setValue(value)
          },
          label:function(value) {
            this.$_controller && this.$_controller.name(value)
          }
        },
        created:function() {
          var self = this
          this.$set(this.context, this.label, this.value)
          this.$_controller = this.$parent.$_gui.addImage(this.context, this.label)
          this.$_controller.placeholder(this.placeholder)
          this.$_controller.onChange(function(img){
            if(img){
              self.$emit('change',img.src)
            }
          })
        }
      })
    }
  }

  if( typeof module === 'object' && typeof module.exports === 'object'){
    module.exports = plugin
  }else if(typeof define === 'function' && define.amd){
    define('VueDatGui', plugin)
  }else{
    root.VueDatGui = plugin
  }

}(this));

<dat-gui :wide='true'を設定すると入力INPUTが幅広くなります

image.png

dat-gui.js
Vue.use(VueDatGui);

興味ある人フォローして!!(´ω`)@https://twitter.com/seraphielbet
ありがとうございました。

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

Let's Vue Programming, 登録画面にDatGui, Enter送りを添えて...by chefよりIを込めて

1.DatGuiってこんなん

こんにちは。職業グラマです。最近ツイッター始めたのをきっかけにQiitaもやってみるか!ってことでトライしてます。ご紹介するのはDatGui。言わずもがなJavaScriptのパラメータ調整用画面をさくっと作れる便利ライブラリです。ゲーム画面やThree.JSを利用した画面なんかでよく利用されています。このFormのパラメータを調整するとJavaScriptのオブジェクト側にも値の変更を行ってくれるのでリアルタイムなゲームの制御また調整ができます。
image.png

2.DatGuiを登録画面に使ってみました

今回私は、このDatGuiをバックエンドのAPIを検証するためのフォームとして利用しました。社員の登録とか削除とか上書するための画面に使います。サクッと作れてコンパクト。だから項目が増えてもBootStrapやSemantic-Uiに比べて見晴らしがよく手間を減らせます。空いた時間にカップラーメン食えるぐらいには良い感じです。
screencapture-127-0-0-1-8888-20200908-dev-2020-09-21-00_42_33.png

3.DatGuiを登録画面にエンター送りを組み込みました

エンター送りってExcelのセルにEnterすると次セルに移動するあれです。伝票とか一日に何度も入力するフォームとかってTabで次の入力インプットに飛ばしたりするの面倒だから項目入力後のEnter Key押されたタイミングで自動的に次インプットにフォーカスを移動する。これがエンター送りです。

社員姓打ち込んでエンターしたら自動的に社員名の入力インプットにフォーカスが移る。って感じです。それを実現するためのコードがこちら。Vueのディレクティブとして構築してます。DatGuiにはtype=textのコントロールが公式にはありませんが私は個人的に組み込みました。動くサンプルとかどっかにあげようと考えてます。

visibleつけているのは、これが入力されているときは次項目を表示しない。といった場面でも正しく動作させるためです。
次INPUTがv-show='false'(display:none;)のときにそれを含まずにキチンと2個目のコントロールにジャンプしてくれます。

vue.directive
   Vue.directive('focus-on-enter-next',function(el,binding,vnode){

     var namespace = binding.arg
     var callback = (binding['value'] !== undefined) ? binding.value : function(){}
     var inputs = $([
       'input[type=password]:visible',
       'input[type=checkbox]:visible',
       'input[type=text]:visible',
       'textarea:visible',
       'select:visible'
     ].join(','),el)

     inputs.off([
       'focus.' + namespace,
       'keydown.' + namespace,
       'focusout.' + namespace
     ].join(' '),el)

     inputs.on('focus.' + namespace,function(ev){
       $(el).closest('.cr').addClass('focus')
     })
     inputs.on('focusout.' + namespace,function(ev){
       $(el).closest('.cr').removeClass('focus')
     })
     inputs.on('keydown.' + namespace,function(ev){
       if(ev.keyCode == 13){
         var i = inputs.index(this)
         var n = (i+1)
         if (n < inputs.length){
           inputs.eq(n).focus()
         }else{
           callback()
         }
         switch(inputs.eq(n).prop('localName')){
           case 'textarea':
           case 'input':
             return false;
         }
       }
     })
   })

4.エンター送りの呼び出し側コード

v-focus-on-enter-next:event_namespace='function_complete'といった形で利用できます。event_namespaceは1画面中にある複数のDatGui毎にエンター送りを適用するためです。function_completeは最後の入力INPUTが完了したタイミングで呼び出されます。ここでは備考です。入力してENTERしたタイミングで呼び出されます。なので入力後に社員登録ボタンを押してAPIを呼び出さなくてもfunction_completeの中に記述しておけば備考までの入力が完了したタイミングで自動的に登録APIを処理できます。楽するためです。

example.html
  <dat-gui v-focus-on-enter-next:employee='complete'>
    <dat-value v-model.number='params.社員コード' label='コード' :min='0' :max='9999' :step='1' :empty_n='0'></dat-value>
    <dat-image v-model='params.社員アバター' label='画像'></dat-image>
    <dat-value v-model='params.社員姓' label='苗字'></dat-value>
    <dat-value v-model='params.社員名' label='名前'></dat-value>
    <dat-value v-model='params.社員姓かな' label='苗字-かな'></dat-value>
    <dat-value v-model='params.社員名かな' label='名前-かな'></dat-value>
    <dat-value v-model='params.社員性別' label='性別' :accepted-values="['男性','女性']"></dat-value>
    <dat-value v-model='params.社員パスワード' label='パスワード'></dat-value>
    <dat-value v-model='params.社員担当部署' label='担当部署' :accepted-values="['管理','営業','開発']"></dat-value>
    <dat-value v-model='params.社員入社日' label='入社日'></dat-value>
    <dat-value v-model='params.社員管理権' label='管理権'></dat-value>
    <dat-text v-model='params.社員備考' label='備考'></dat-text>
 </dat-gui>

5.DatGui Vue Plugin忘れてました

lq111lq/vue-dat-guiを1ファイル化してTextコントローラを入れた版です。その他empty_nとかwideとかplaceholderとかreadonlyとかpasswordとか公式にはないプロパティは私個人が組み込んだものになります。

wide属性をdat-guiタグで有効(true)にすればDatGui左側のラベルが消えて幅広く入力できます。
ラベル名はそのままInputのplaceholderに適用されます。
password属性をdat-valueタグで有効にすれば入力内容が****として隠されます。
readonly属性は編集不可にするだけです。社員に自動付与されたIDの表示なんかに使います。
empty_nはスライダーボックスでempty_nのnの値の時、ボックス内を空文字にしてくれます。自分で社員IDを打ち込むときに使います。

あと、DatGuiのプルリクエストにあったイメージコントローラとか、スライダーボックス上でマウスホイール又は上下キーでカウントアップ/カウントダウンを組み込んでます。これで登録も楽になるかなーと考えてました。

vue.directive
(function(root){

  var plugin={
    install:function(Vue,options){

      Vue.component('DatGui',{
        template:"\
          <div>\
            <div ref='container'>\
              <slot></slot>\
           </div>\
         </div>\
        ",
        name:'DatGui',
        props: {
          wide: {
            type: Boolean,
            default: false
          }
        },
        watch: {
          wide:function(value) {
            this.$_gui && this.$_gui.wide(value)
          }
        },
        data:function(){
          return {
            context: {}
          }
        },
        provide:function() {
          return {
            context: this.context
          }
        },
        created:function() {
          this.$_gui = new dat.GUI({ autoPlace: false })
        },
        mounted:function() {
          this.$refs.container.appendChild(this.$_gui.domElement)
          this.$_gui.wide(this.wide)
        },
        beforeDestroy:function() {
          this.$_gui && this.$_gui.destroy()
        }
      })
      Vue.component('DatFolder',{
        template:"\
          <span>\
            <slot>\
           </slot>\
         </span>\
        ",
        props: {
          name: {
            type: String,
            default: 'folder'
          },
          wide: {
            type: Boolean,
            default: false
          }
        },
        watch: {
          wide:function(value) {
            this.$_gui && this.$_gui.wide(value)
          }
        },
        created:function() {
          this.$_gui = this.$parent.$_gui.addFolder(this.name)
        },
        mounted:function() {
          this.$_gui.wide(this.wide)
        }
      })
      Vue.component('DatValue',{
        template:"\
          <span>\
         </span>\
        ",
        name: 'DatValue',
        props: {
          value: {
            type: [Number, String, Boolean, undefined],
            default: undefined
          },
          label: {
            type: String
          },
          max: {
            type: Number,
            default: null
          },
          min: {
            type: Number,
            default: null
          },
          step: {
            type: Number,
            default: null
          },
          empty_n: {
            type: Number,
            default: null
          },
          acceptedValues: {
            type: [Object, Array],
            default:function() {
              return null
            }
          },
          placeholder: {
            type: String,
            default: ''
          },
          readonly: {
            type: Boolean,
            default: false
          },
          password: {
            type: Boolean,
            default: false
          }
        },
        inject: ['context'],
        computed: {
          valueInner:function() {
            return this.context && this.context[this.label]
          }
        },
        watch: {
          value:function(value) {
            this.$_controller && this.$_controller.setValue(value)
          },
          valueInner:function(newVal) {
            switch(true){
              case this.$_controller instanceof dat.controllers.NumberController:
              case this.$_controller instanceof dat.controllers.NumberControllerBox:
                this.$emit('input', newVal <= this.empty_n ? '' : newVal)
                break;
              default:
                this.$emit('input', newVal)
            }
          },
          label:function(value) {
            this.$_controller && this.$_controller.name(value)
          },
          max:function(value) {
            this.$_controller && this.$_controller.max(value)
            this.$_controller.updateDisplay()
          },
          min:function(value) {
            this.$_controller && this.$_controller.min(value)
            this.$_controller.updateDisplay()
          },
          step:function(value) {
            this.$_controller && this.$_controller.step(value)
            this.$_controller.updateDisplay()
          },
          empty_n:function(value){
            this.$_controller && this.$_controller.empty_n(value)
            this.$_controller.updateDisplay() 
          },
          acceptedValues:function(list) {
            var html = ''
            var type = Object.prototype.toString.call(list).slice(8,-1).toLowerCase()

            switch(type){
              case 'array':
                list.forEach(function(v){
                  html += '<option value=' + v + '>' + v + '</option>';
                })
                break;
              case 'object':
                Object.keys(list).forEach(function(k){
                  html += '<option value=' + list[k] + '>' + k + '</option>';
                })
                break;
            }

            if(this.$_controller){
              this.$_controller.domElement.children[0].innerHTML = html
            }
          },
          placeholder:function(value) {
            this.$_controller && this.$_controller.placeholder(value)
          },
          readonly:function(value) {
            this.$_controller && this.$_controller.readonly(value)
          },
          password:function(value) {
            this.$_controller && this.$_controller.password(value)
          }
        },
        created:function() {
          var self = this

          if (this.value === undefined) {
            this.$set(this.context, this.label, function() {
              self.$emit('click')
            })
            this.$parent.$_gui.add(this.context, this.label)
            return
          }

          this.$set(this.context, this.label, this.value)
          if (this.acceptedValues) {
            this.$_controller = this.$parent.$_gui.add(this.context, this.label, this.acceptedValues)
            this.$_controller.onFinishChange(function(obj){
              var has_selected_option = this.__select.selectedOptions.length > 0
              if(has_selected_option){
                self.$emit('change',this.__select.selectedOptions[0].innerText)
              }
            })
          } else if (this.min != null && this.max != null && this.step != null) {
            if (this.empty_n != null){
              this.$_controller = this.$parent.$_gui.add(this.context, this.label, this.min, this.max, this.step, this.empty_n)
            } else {
              this.$_controller = this.$parent.$_gui.add(this.context, this.label, this.min, this.max, this.step)
            }

            this.$_controller.onChange(function(value){
              self.$emit('change',value)
            })
          } else {
            this.$_controller = this.$parent.$_gui.add(this.context, this.label)
            this.$_controller.placeholder(this.placeholder)
            this.$_controller.readonly(this.readonly)
            this.$_controller.password(this.password)
          }
        }
      })
      Vue.component('DatColor',{
        template:"\
          <span>\
         </span>\
        ",
        name: 'DatColor',
        props: {
          value: {
            type: [String, Array, Object]
          },
          label: {
            type: String
          }
        },
        inject: ['context'],
        computed: {
          valueInner:function() {
            return this.context && this.context[this.label]
          }
        },
        watch: {
          value:function(value) {
            this.$_controller && this.$_controller.setValue(value)
          },
          valueInner:function(newVal) {
            this.$emit('input', newVal)
          }
        },
        created:function() {
          this.$set(this.context, this.label, this.value)
          this.$_controller = this.$parent.$_gui.addColor(this.context, this.label)
        }
      })
      Vue.component('DatText',{
        template:"\
          <span>\
         </span>\
        ",
        name: 'DatText',
        props: {
          value: {
            type: String
          },
          label: {
            type: String
          },
          placeholder: {
            type: String,
            default: ''
          },
          readonly: {
            type: Boolean,
            default: false
          }
        },
        inject: ['context'],
        computed: {
          valueInner:function() {
            return this.context && this.context[this.label]
          }
        },
        watch: {
          value:function(value) {
            this.$_controller && this.$_controller.setValue(value)
          },
          valueInner:function(newVal) {
            this.$emit('input', newVal)
          },
          label:function(value) {
            this.$_controller && this.$_controller.name(value)
          },
          placeholder:function(value) {
            this.$_controller.placeholder(value)
          },
          readonly:function(value) {
            this.$_controller.readonly(value)
          }
        },
        created:function() {
          this.$set(this.context, this.label, this.value)
          this.$_controller = this.$parent.$_gui.addText(this.context, this.label)
          this.$_controller.placeholder(this.placeholder)
          this.$_controller.readonly(this.readonly)
        }
      })
      Vue.component('DatImage',{
        template:"\
          <span>\
         </span>\
        ",
        name: 'DatImage',
        props: {
          value: {
            type: String
          },
          label: {
            type: String
          },
          placeholder: {
            type: String,
            default: ''
          }
        },
        inject: ['context'],
        computed: {
          valueInner:function() {
            return this.context && this.context[this.label]
          }
        },
        watch: {
          value:function(value) {
            this.$_controller && this.$_controller.setValue(value)
          },
          label:function(value) {
            this.$_controller && this.$_controller.name(value)
          }
        },
        created:function() {
          var self = this
          this.$set(this.context, this.label, this.value)
          this.$_controller = this.$parent.$_gui.addImage(this.context, this.label)
          this.$_controller.placeholder(this.placeholder)
          this.$_controller.onChange(function(img){
            if(img){
              self.$emit('change',img.src)
            }
          })
        }
      })
    }
  }

  if( typeof module === 'object' && typeof module.exports === 'object'){
    module.exports = plugin
  }else if(typeof define === 'function' && define.amd){
    define('VueDatGui', plugin)
  }else{
    root.VueDatGui = plugin
  }

}(this));

<dat-gui :wide='true'を設定すると入力INPUTが幅広くなります

image.png

dat-gui.js
Vue.use(VueDatGui);

興味ある人フォローして!!(´ω`)@https://twitter.com/seraphielbet
ありがとうございました。

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