はじめに
HTML/CSS/JavaScriptで10個(くらい)何かを作ってみるチャレンジの一環として、
仮想キーボードを作っていきます。
目的
何か作ってみるチャレンジの目的は、
HTML/CSS/Vanilla JSを使ってとりあえずなにか作ること、手を動かすことです。
これでは曖昧すぎるので、もう少し深掘ります。
現状
- HTML/CSS/JavaScriptについて、コードを見ればなんとなく理解出来る。
- チュートリアルと一緒に進めれば何かは作れる。
- 0から何か作ろうと思うと厳しい。
- Python(業務効率化などで使用)の場合はある程度出来る。
なぜ厳しいのか?を考えた時に、
Pythonとの違いは実際に手を動かしているかどうかだなと思いました。
プログラミングにおいて手を動かすに勝る学習方法はないと思います。
実感としてもそうですし、そう言っているエンジニアの方も多く感じます。
また、質を高めるためにも、まずは絶対的な量が必要とよく言います。
というわけで、色々作ってみることにしました。
ルール
- 期間は3週間くらい。
- 数は10個くらい。
- お題はパクリでもOK。
- ただし、教材に従って進めるだけ、はNG。アイデアは持ってきてもいいし参考にするのは良いが、まずは自分で考える。
- 人のコードを参考にしても良いが、ただのコピペはNG。何をしているのか理解すること。
また、今回のように動画を参考にして作る場合は以下のような流れで進めました。
- 1セクションごとに自分ならどうするか考える
- ざっくり実装したり、処理をコメントで書いたりしてみる
- 動画を見ながら修正
参考
以下から面白そうなお題をいくつか参考にします。
- ポートフォリオに役立つJavaScriptプロジェクト40選(動画あり) - Qiita
- JavaScript 30
- JavaScript Web Projects: 20 Projects to Build Your Portfolio | Udemy
リポジトリ
仮想キーボード
概要
下の動画を参考に画面上で操作出来る仮想キーボードを実装していきます。
主な目的
- 仮想キーボードの作り方を学ぶ
- キーボードのように若干複雑なレイアウトをどのように実現するのか学ぶ
- コードの書き方を参考にする
完成イメージ
HTML実装
早速実装していきます。
とりあえずHTML。
<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title> Virtual Keyboard HTML/CSS/JS</title>
<link rel="stylesheet" href="assets/keyboard.css">
<link rel="shortcut icon" href="assets/favicon.ico">
</head>
<body>
<h1>Virtual Keyboard HTML/CSS/JS</h1>
<h3>Features</h3>
<ul>
<li>Easy to integrate</li>
<li>Responsive</li>
<li>Vanilla JS( <strong>no libraries required!</strong> )</li>
</ul>
<textarea style="position: absolute; top: 130px; right: 30px; width: 300px;"></textarea>
</body>
</html>
textarea
に対してインラインCSSを書き、スタイルを割り当てます。
本来キーボード部分はJavaScriptで作りますが、今は存在しません。
CSSを編集したいのでダミーのHTMLを作って行きます。
具体的には、キーボードのコンテナ、キーボード用の div
を作ります。
そして、その中にキーボード( button type="button"
)を入れていきます。
<div class="keyboard">
<div class="keyboard__keys">
<button type="button" class="keyboard__key">a</button>
</div>
</div>
Google Material icon
特殊なキーの表現にはGoogle Material iconを使っていくようです。
HTMLにリンクを追加して使えるようにします。
<link href="https://fonts.googleapis.com/icon?family=Material+Icons"
rel="stylesheet">
CSS実装
キーボードコンテナ
.keyboard {
position: fixed;
left: 0;
bottom: 0;
width: 100%;
padding: 5px 0;
background-color: #53a6bb;
box-shadow: 0 0 50px rgba(0, 0, 0, 0.5);
user-select: none;
transition: bottom 0.4s;
height: 100px;
}
position: fixed
スクロールしてもキーボードが表示され続けるようにする。
padding: 5px 0;
参考 : padding - CSS: カスケーディングスタイルシート | MDN
一括指定。この場合は左右5px
、上下0
。
background-color: #004134;
背景色の指定。
box-shadow: 0 0 50px rgba(0, 0, 0, 0.5);
参考 : box-shadow - CSS: カスケーディングスタイルシート | MDN
以下のような構文で使用します。
blur-radius
は値が大きくなるほどぼかしが大きくなり、影の面積が広く・色が薄くなります。
/* キーワード値 */
box-shadow: none;
/* offset-x | offset-y | color */
box-shadow: 60px -16px teal;
/* 今回はこれ */
/* offset-x | offset-y | blur-radius | color */
box-shadow: 10px 5px 5px black;
/* offset-x | offset-y | blur-radius | spread-radius | color */
box-shadow: 2px 2px 2px 1px rgba(0, 0, 0, 0.2);
/* inset | offset-x | offset-y | color */
box-shadow: inset 5em 1em gold;
/* 複数の影はカンマで区切る */
box-shadow: 3px 3px red, -1em 0 0.4em olive;
user-select: none;
参考 : user-select - CSS: カスケーディングスタイルシート | MDN
ユーザーのテキスト範囲選択を禁止します。
transition: bottom 0.4s;
キーボードが下から出てくるアニメーション。
キーボードを隠す
.keyboard--hidden
という名称のクラスを使います。
.keyboard--hidden {
bottom: -100%;
}
bottom: -100%
を使用、JSによりクラスを add/remove
することで見える状態⇔見えない状態を切り替える。
BEMについて
動画内でBEMという命名規則に従う、といった趣旨の話をされていました。
調べてみます。
参考
BEMとは
BEMはBlock Element Modifierの略で、CSSを設計・命名していく手法です。
- Block: 大枠となる独立した要素
- Element: Block中の要素
- Modifier: BlockやElementのスタイル
命名ルール
クラスであれば以下のようになります。
class=”block__element—modifier”
Block: 大枠となる独立した要素
Element: Block中の要素
Modifier: BlockやElementのスタイル
これらの要素を以下のルールに基づいて命名しています。
- blockとelementはアンダースコア2つで区切る
- modifierはハイフン2つで区切る
- block, element, modifierが複数の単語で構成される場合、単語間はハイフン1つで区切る
ファイル
1blockにつき1ファイル。
block名.css
とする。
CSSは全ての名前がグローバルなため, 命名が重複するとメンテナンス性が著しく低下するのが弱点であるが, BEMは命名規則を厳しく縛ることによってこの弱点を克服しており, element名はいくら重複しても問題ない.ただし, block名が重複してしまうとせっかく克服したものが台無しになってしまうため絶対に避けなければならない.1ファイルにつき1blockしか定義せずファイル名をblock名にする規則を守ってさえいれば, block名が重複する心配が無い.
仮想キーボードでは keyboard
がblock、--hidden
がmodifierですね。
キー
キーのスタイル
キーそれぞれに対してスタイルを追加していきます。
- キー全て
.keyboard__keys {
text-align: center;
}
テキストが中心になるようにします。
- それぞれのキー
.keyboard__key {
height: 45px;
width: 6%;
max-width: 90px;
margin: 3px;
border-radius: 4px;
border: none;
background: rgba(255, 255, 255, 0.2);
color: #ffffff;
}
width 6%
max-width: 90px
レスポンシブになるように。
また、画面からはみ出さないように。
border-radius: 4px;
border: none;
角を丸め、線を消す。
background: rgba(255, 255, 255, 0.2)
.keyboard
で設定した背景色よりも明るい色になるように。
更に追加します。
font-size: 1.05rem
ルートサイズより5%大きく
outline: none
参考 : outline - CSS: カスケーディングスタイルシート | MDN
borderとoutlineの違いは以下です。
境界線と輪郭線はとても似ています。しかし、輪郭線は以下の点で境界線とは異なります。
輪郭線は領域を占有せず、要素のコンテンツの外側に描かれます。- 仕様によれば、輪郭線は矩形である必要はありませんが、ふつうは矩形です。
他の一括指定プロパティと同様に、省略された値は初期値に設定されます。
border
outline
まで翻訳されてしまっていて若干わかりにくいですが、 outline
は要素の外側に描画されるそうです。
inline-flex
キーの中にアイコンを置くことがあり、それらを中央寄せするために設定します。
参考 : display - CSS: カスケーディングスタイルシート | MDN
インライン要素のような振る舞いをしつつ、中身を flex
に従ってレイアウトします。
キーにアイコン表示
Google Material iconを使ってアイコンを置いてみます。
<button type="button" class="keyboard__key">
<i class="material-icons">backspace</i>
</button>
え?これだけ?という感じなのですが、
ちゃんと追加出来てます。すごい。
ただ、見ての通りアイコンの方が若干ずれているので修正していきます。
・・・
vertical-align: top;
padding: 0;
-webkit-tap-highlight-color: transparent;
}
vertical-align
参考 : vertical-align | MDN
vertical-align は、2 つの場面で使用することができます。
包含する行ボックスの中で、インライン要素のボックスの垂直方向の配置を決める場合。例えば、テキストの行の中で画像の垂直位置を決めるために使用することができます。
表のセルの内容の垂直方向の配置を決める場合。
vertical-align
はインライン要素、インラインブロック要素、表のセル要素だけに適用されることに注意してください。つまり、ブロックレベル要素の垂直方向の配置には使用できません。
webkit-tap-highlight-color
参考 : webkit-tap-highlight-color
標準ではないプロパティです。Firefox、Safariではサポートされていません。
webkit-tap-highlight-color
は CSS の標準外のプロパティで、リンクがタップされている間に表示される強調色を設定します。強調は、ユーザーがタップしたことが正常に認識されていることを示し、またどの要素がタップされているかを示します。webkit-tap-highlight-color: red; -webkit-tap-highlight-color: transparent; /* 強調をなくす */
最後に、 position: relative
を追加。
capslockのアクティブ/非アクティブを示すライトのためらしい。
absolute
だと親からの位置になってしまって、ライトが上手く配置出来ないのだと思われる。
キーがアクティブなとき
.keyboard__key:active {
background: rgba(255, 255, 255, 0.12);
}
若干暗くする。
特殊なキー
- 若干幅のあるキー
- かなり幅のあるキー
- 色が暗いキー
用のCSSを追加します。
HTMLにアイコン、クラスを追加します。
<button type="button" class="keyboard__key keyboard__key--wide">
<i class="material-icons">backspace</i>
</button>
<button type="button" class="keyboard__key keyboard__key--extra-wide">
<i class="material-icons">space_bar</i>
</button>
.keyboard__key--wide {
width: 12%;
}
.keyboard__key--extra-wide {
width: 36%;
max-width: 500px;
}
width
を変更します。
.keyboard__key--extra-wide
ははみ出さないようにmax-width
を指定します。
capslockのライトを実装します。
keyboard__key--activatable
というクラスを持つcapslockキーを実装し、以下のスタイルを追加します。
.keyboard__key--activatable::after {
content: "";
top: 10px;
right: 10px;
position: absolute;
width: 8px;
height: 8px;
background: rgba(0, 0, 0, 0.4);
border-radius: 50%;
}
絶対位置指定で親(キー)の右上に配置し、スタイルを与えます。
また、 keyboard__key--active
を追加、ライトをアクティブにできるようにします。
.keyboard__key--active::after {
background : #08ff00;
}
暗いキーは以下のように実装。
.keyboard__key--dark {
background: rgba(0, 0, 0, 0.5);
}
JavaScript実装
Keyboardオブジェクト
const Keyboard = {
elements: {
main: null,
keysContainer: null,
keys: []
},
eventHandlers: {
oninput: null,
onclose: null
},
properties: {
// キーボードの入力
value: "",
capsLock: false
}
};
キーボードを表すために必要なKeyboard
オブジェクトを定義していきます。
elements
メイン要素(キーボード)、キーボードのコンテナ、それぞれのキー
eventHandlers
イベントハンドラー
properties
キーボードの入力値、capsLockのアクティブ状態
また、このオブジェクトは以下のメソッドを持ちます。
_
から始まるメソッドはプライベートメソッドです。
init() {
},
_createKeys() {
},
_triggerEvent(handlerName) {
console.log("Event Triggered! Event Name:" + handlerName);
},
_toggleCapsLock() {
console.log("Caps Lock Toggled!");
},
open(initialValue, oninput, onclose) {
},
close() {
}
open
メソッドにはinitialVale
を渡します。
これは textarea
に既に入力があった場合、その入力をスタート値にするためです。
init
全てのDOMがロードされたときをトリガーに init
を実行します。
// 全てのDOMがロードされた時
window.addEventListener("DONContentLoaded", function() {
Keyboard.init();
})
init() {
// main要素を作成
this.elements.main = document.createElement("div");
this.elements.keysContainer = document.createElement("div");
// main要素をセットアップ
this.elements.main.classList.add("keyboard", "1keyboard--hidden");
this.elements.keysContainer.classList.add("keyboard__keys");
// DOMの追加
this.elements.main.appendChild(this.elements.keysContainer);
document.body.appendChild(this.elements.main);
},
elements
に生成したDOM要素を代入、 document
に appendChild
していきます。
なるほど、オブジェクトに生成した要素を持たせる感じなんですね。
keyboard--hidden
の頭に1をつけているのは、開発用(開発中は見えるようにしておく)だから。
こういった小技も勉強になります。
DOMContentLoaded
参考
- Window: DOMContentLoaded イベント - Web API | MDN
- ページのライフサイクル: DOMContentLoaded, load, beforeunload, unload
- DOMContentLoaded周りの処理を詳しく調べてみました - Qiita
- javascript - Difference between DOMContentLoaded and load events - Stack Overflow
HTMLの解析、DOMツリーの構築が完了した段階で発生するイベントです。
img
のような外部リソース、スタイルシートはまだ読み込まれていない可能性があります。
それに対してload
は、ブラウザが画像やスタイルなどを含めたすべてのリソースを読み込んだ段階で発生するイベントです。
window
でのload
イベントはページとすべてのリソースがロードされたときにトリガされます。通常はこんなに長く待つ必要はないため、めったに使われません。ページのライフサイクル: DOMContentLoaded, load, beforeunload, unload より
別なイベントである loadは、ページ全体が読み込まれたことを検出するためにのみ使用してください。
load
を、DOMContentLoaded
がより適切である場面に使用する間違いがよくあります。
上のような記述から、
- 特別な理由がない限り
DOMContentLoaded
を使う - 何かしら(画像を操作したいなど)の理由があれば
load
を使う
という感じでしょうか。
_createKeys
DocumentFragment
を使ってHTML要素を追加していきます。
DocumentFragment
は仮想的なDOMツリーを生成するようなイメージで使います。
主に大量の要素を一気に追加したい時なんかに使います。
_createKeys() {
const fragment = document.createDocumentFragment();
const keyLayout = [
"1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "backspace",
"q", "w", "e", "r", "t", "y", "u", "i", "o", "p",
"caps", "a", "s", "d", "f", "g", "h", "j", "k", "l", "enter",
"done", "z", "x", "c", "v", "b", "n", "m", ",", ".", "?",
"space"
];
const createIconHTML = (icon_name) => {
return `<i class="material-icons>${icon_name}</i>`
}
keyLayout.forEach(key => {
const keyElement = document.createElement("button");
const insertLineBreak = ["backspace", "p", "enter", "?"].indexOf(key) !==-1;
// 属性/クラスを追加
keyElement.setAttribute("type", "button");
// 全てのキーに必要なクラス
keyElement.classList.add("keyboard__key");
列区切り
const insertLineBreak = ["backspace", "p", "enter", "?"].indexOf(key) !==-1;
...
if(insertLineBreak) {
fragment.appendChild(document.createElement("br"));
}
キーボードの列区切りはindexOf
を使って判定、 <br>
タグで改行を入れることで表現します。
String.prototype.indexOf() - JavaScript | MDN
indexOf
は見つからなければ -1
を返すので、見つかった場合は innerLineBreak
が true
になります。
特殊なキー
switch-case
を使います。
switch(key) {
case "backspace":
keyElement.classList.add("keyboard__key--wide");
keyElement.innerHTML = createIconHTML("backspace");
keyElement.addEventListener("click", () => {
this.properties.value = this.properties.value.substring(0, this.properties.length - 1);
this._triggerEvent("oninput");
})
break;
case "caps":
keyElement.classList.add("keyboard__key--wide", "keyboard__key--activatable");
keyElement.innerHTML = createIconHTML("keyboard_capslock");
keyElement.addEventListener("click", () => {
this._toggleCapsLock();
keyElement.classList.toggle("keyboard__key--active", this.properties.capsLock);
})
break;
case "enter":
keyElement.classList.add("keyboard__key--wide");
keyElement.innerHTML = createIconHTML("keyboard_return");
keyElement.addEventListener("click", () => {
this.properties.value += "\n";
this._triggerEvent("oninput");
})
break;
case "space":
keyElement.classList.add("keyboard__key--extra--wide");
keyElement.innerHTML = createIconHTML("space_bar");
keyElement.addEventListener("click", () => {
this.properties.value += " ";
this._triggerEvent("oninput");
})
break;
case "done":
keyElement.classList.add("keyboard__key--wide", "keyboard--dark");
keyElement.innerHTML = createIconHTML("check_circle");
keyElement.addEventListener("click", () => {
this.close();
this._triggerEvent("onclose");
})
break;
やっていることは単純で、
- 必要なクラスとアイコンを追加
- アイコンの生成は専用の関数を作っておいて、それを利用します。
- クリックされた時の処理を登録
しているだけです。
backspace
substring
を使って1文字削除しています。
caps
toggle
を使ってアクティブにします。
DOMTokenList.toggleは渡された token
をリストから削除、 false
を返します。
token
が存在しなかった場合は、追加して true
を返します。
done
入力終了を表すキーです。close
を呼びます。
通常のキー
default:
keyElement.textContent = key.toLowerCase();
keyElement.addEventListener("click", () => {
this.properties.value += this.properties.capsLock ? key.toUpperCase() : key.toLowerCase();
this._triggerEvent("oninput");
})
break;
三項演算子を使ってcapsLock
がアクティブなら大文字に、そうでなければ小文字にします。
これでキーボードが完成・・・
あれ。
innerHTML
だけ空で、 createIcon
などの関数は正常に動いているように見えたのでそこそこ解決に時間がかかってしまいましたが、
const createIconHTML = (icon_name) => {
return `<i class="material-icons>${icon_name}</i>`;
}
material-icons
の ”
が足りていないだけでした。
補完頼りはよくないですね・・・
その他クラス名を間違えていてスタイルが適用されていない箇所などあったので修正しました。
結果が以下です。
OKですね。
capslockがONになったときの挙動
this.elements.keys = this.elements.keysContainer.querySelectorAll(".keyboard__key");
capslock
による制御で使うために、 Keyboard.keys
に全てのキー要素を追加します。
通常のキーはアイコンを持たないので、子要素を持ちません。
これを利用して表示を更新します。
_toggleCapsLock() {
console.log("Caps Lock Toggled!");
this.properties.capsLock = !this.properties.capsLock;
for (const key of this.elements.keys) {
if(key.childElementCount === 0) {
key.textContent = this.properties.capsLock ? key.textContent.toUpperCase() : key.textContent.toLowerCase();
}
}
}
三項演算子は今回のようにある入力を元にして値がスイッチするような場合に便利そうですね。
イベントの制御
_triggerEvent(handlerName) {
console.log("Event Triggered! Event Name:" + handlerName);
if(typeof this.eventHandlers[handlerName] == "function") {
this.eventHandlers[handlerName](this.properties.value);
}
},
_triggerEvent
に渡された引数が eventHandlers
に存在する関数であれば、 現在のvalue
を渡しつつ呼び出します。
_triggerEvent
関数は、入力キーのclick
イベントに登録されています。
case "backspace":
keyElement.classList.add("keyboard__key--wide");
keyElement.innerHTML = createIconHTML("backspace");
keyElement.addEventListener("click", () => {
this.properties.value = this.properties.value.substring(0, this.properties.length - 1);
this._triggerEvent("oninput");
})
break;
open(initialValue, oninput, onclose) {
this.properties.value = initialValue || "";
this.eventHandlers.oninput = oninput;
this.eventHandlers.onclose = onclose;
this.elements.main.classList.remove("keyboard--hidden");
},
close() {
this.value = "";
this.eventHandlers.oninput = oninput;
this.eventHandlers.onclose = onclose;
this.elements.main.classList.add("keyboard--hidden")
}
open
close
にはキーボードがアクティブ/非アクティブになったときの処理が記述されています。
具体的には、
-
value
のリセット -
eventHandlers
のリセット -
"keyboard--hidden"
クラスの制御
です。
document.querySelectorAll(".use-keyboard-input").forEach(element => {
element.addEventListener("focus", () => {
this.open(element.value, currentValue => {
element.value = currentValue;
});
});
})
最後に textarea
の focus
イベントに open
を登録します。
focus
された時、open(initialValue, oninput, onclose
) に対して
-
element.value
(initialValue
) - 引数として渡された値を
element.value
に代入する関数
をそれぞれ渡しています。
無事完成です!
まとめ
キーボードを Keyboard
オブジェクトとして表現していく流れが綺麗すぎて感動しました。
また、
VSCodeでHTMLを書く時、.keyboard__keys
と打つと自動的に div class="keyboard__keys"
が生成される
など、テクニック的な面でも参考になる箇所がいくつかありました。
ひょっとして常識なんでしょうか。初めて知りました。
解説がかなり丁寧で、英語も聞き取りやすいです。
非常に良い動画でした。
学んだこと
- 仮想キーボードの考え方を理解した
- アクティブ/非アクティブ状態を持つことで表示を制御
- オブジェクトとしてキーの状態や入力を保持することでキーボードを表現する
- 実装面
- 仮想キーボードのUI実装
- BEMについて
- Google Material Iconを使うと簡単にアイコンを扱える
-
load
DOMContentLoaded
の違い - オブジェクトの扱い
- JavaScriptを使ってHTMLを描画する方法