「今日の勤務時間、実働何時間だったっけ?」「この時給で、今日の稼ぎはいくらになるんだろう?」
そんな日々の疑問を解決する時間計算ツールを、自分の手で作ってみませんか?
今回ご紹介するのは、HTMLとJavaScriptだけで作れる「開始時刻・終了時刻・休憩時間・時給」から作業時間と報酬を自動計算するWebツールの作り方です。
しかも、計算履歴をブラウザに保存して、あとから呼び出す機能までついています。
「プログラミングは初心者だけど、自分の役に立つツールを作ってみたい」
「実用的なスキルを身につけながら、コードに触れてみたい」
そんな方にピッタリの内容です。
この記事では、ツールの仕組みからHTML・CSS・JavaScriptの具体的なコードまで、誰でも作れるように丁寧に解説していきます。
コピペから始めてもOK!
このツールを通して、「作れた!」という達成感とWeb開発の楽しさをぜひ体感してください。
【まずは時間計算ツールの完成版を試してみたい方はこちら】▼
なぜ今、Webツールを「自作」すべきなのか?その魅力とメリット
「プログラミング」と聞くと、なんだか難しそう…と思う方もいるかもしれません。でも、心配いりません!今回ご紹介するようなシンプルなWebツールなら、HTMLとJavaScriptだけで、特別なソフトも必要なく、誰でも手軽に作れてしまうんです。
Webツールを自分で作ることには、たくさんの魅力とメリットがあります。
- 日々の「ちょっと不便」を自分で解決できるスキルが身につく:
「こんな機能があったらいいのに…」という小さなアイデアを、自分で形にできるようになります。 - プログラミング学習の最高のモチベーションになる「作れた!」という体験:
書いたコードが実際に動いて、誰かの役に立つツールになる喜びは、何物にも代えがたい学習の原動力になります。 - ブラウザだけで動くため、誰でも簡単に共有・利用できる汎用性:
作ったツールはインターネット環境があればどこでも使え、友人や同僚と簡単に共有できます。 - 初期費用ゼロで始められる手軽さ:
必要最低限の準備で、すぐに開発をスタートできます。
この記事を読み進めれば、実際にツールを動かすための基本的なHTMLとJavaScriptの記述方法を学び、最終的にはあなただけの「作業時間・休憩時間計算ツール」を作成できるようになります。
ツールの全体像と開発環境の準備
まず、今回作成するツールの全体像と、開発を始めるために必要なものを見ていきましょう。
ツールの動作原理の概要
この作業時間計算ツールは、非常にシンプルな仕組みで動いています。
- あなたがWebブラウザの画面で、開始時刻や終了時刻などの情報を入力します。
- その入力された情報をJavaScriptというプログラミング言語が受け取ります。
- JavaScriptが時刻の計算や休憩時間の差し引き、時給換算といった複雑な処理を行います。
- 計算された結果(実働時間や純作業時間)を、再びHTMLというマークアップ言語で構成された画面上に表示します。
- さらに、計算履歴の保存には、ブラウザが標準で持っているローカルストレージという機能を使います。これにより、あなたがツールを閉じてもデータが失われることなく、次回アクセス時に以前の履歴を確認できます。
開発環境の準備
「何か特別なソフトが必要なの?」と思うかもしれませんが、ご安心ください。必要なものは、ほとんどのパソコンに最初から入っているものです。
- テキストエディタ:
コードを書くためのソフトです。Windowsなら「メモ帳」、macOSなら「テキストエディット」でも良いですが、プログラミング学習には「VS Code (Visual Studio Code)」が断然おすすめです!無料で高機能、そして多くのプログラマーが使っています。 - Webブラウザ:
作成したツールを実際に動かして確認するためのものです。Google Chrome、Mozilla Firefox、Microsoft Edgeなど、普段お使いのブラウザでOKです。
これだけです!今回は work_time_calculator.html
のような単一のHTMLファイル内で、HTML、CSS、JavaScriptのすべてを記述していきます。そのため、特別なサーバーの準備なども一切不要です。
難しい環境構築は一切ナシ。ブラウザとメモ帳だけでOK!

HTMLで画面の骨格を作ろう(UIの構築)
それでは早速、ツールの「見た目」となる部分、ユーザーインターフェース(UI)をHTMLで構築していきましょう。
まずは以下の基本的なHTML構造から始めます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>シンプルな作業時間・休憩時間 計算ツール</title> <style> /* スタイルは後ほど追加します */ </style> </head> <body> <div class="container"> <h1>シンプルな作業時間・休憩時間 計算ツール</h1> <div class="input-group"> <label for="startTime">開始時刻:</label> <input type="time" id="startTime" value="09:00"> </div> <div class="input-group"> <label for="endTime">終了時刻:</label> <input type="time" id="endTime" value="17:00"> </div> <div class="checkbox-group"> <input type="checkbox" id="includeBreak"> <label for="includeBreak">休憩時間を差し引く</label> </div> <div class="input-group" id="breakTimeGroup" style="display: none;"> <label for="breakTime">休憩時間:</label> <input type="time" id="breakTime" value="01:00"> </div> <div class="input-group"> <div class="hourly-wage-group"> <label for="hourlyWage">時給 (任意):</label> <input type="number" id="hourlyWage" placeholder="例: 1000" min="0"> <span>円</span> </div> </div> <button id="calculateBtn">計算する</button> <div class="results" id="results"> <p>実働時間: <span id="actualDuration">0時間0分</span></p> <p>純作業時間: <span id="pureDuration">0時間0分</span></p> <p id="hourlyWageResult" style="display: none;">時給換算: <span id="wageAmount">0円</span></p> </div> </div> <div class="history-container"> <h2>計算履歴</h2> <ul id="history-list"> <p class="no-history">まだ履歴がありません。</p> </ul> <button id="clear-history-btn">履歴をすべて削除</button> </div> <script> // JavaScriptは後ほど追加します </script> </body> </html> |
このコードを work_time_calculator.html
などの名前で保存し、ブラウザで開いてみてください。まだデザインは適用されていませんが、各入力欄やボタン、結果表示エリアの骨組みが確認できるはずです。
基本的なHTML構造
<!DOCTYPE html>
これがHTML5で書かれたドキュメントであることを宣言します。<html lang="ja">
HTMLドキュメントの開始を宣言し、言語を日本語に設定しています。<head>
ブラウザには表示されない、ページに関するメタ情報(文字コード、レスポンシブ対応、タイトルなど)を記述します。<meta charset="UTF-8">
文字化けを防ぐために必須です。<meta name="viewport" ...>
スマートフォンなど、さまざまな画面サイズで適切に表示されるように設定します(レスポンシブデザインの基本)。<title>
ブラウザのタブに表示されるページタイトルです。<style>
後ほどCSSをここに記述します。
<body>
実際にブラウザに表示されるすべてのコンテンツを記述します。
入力フォームの作成
<div class="input-group">
入力欄とラベルをひとまとめにするためのグループです。CSSでデザインを整える際に便利です。<label for="ID名">
入力欄(input
)と紐付けて、その入力欄が何のためのものかをユーザーに示します。for
属性の値は、対応するinput
タグのid
と一致させる必要があります。<input type="time" id="startTime" value="09:00">
時刻を入力するための入力欄です。type="time"
とすることで、ブラウザが時刻入力に適したインターフェースを提供してくれます。id
はJavaScriptからこの要素を操作するために使います。value
は初期表示される値です。<input type="checkbox" id="includeBreak">
チェックボックスです。「休憩時間を差し引く」機能のオン/オフに使います。<input type="number" id="hourlyWage" placeholder="例: 1000" min="0">
数値を入力するための入力欄です。type="number"
とすることで、数字以外の入力が制限されます。min="0"
は最小値を0に設定しています。<button id="calculateBtn">計算する</button>
計算を実行するためのボタンです。
結果表示エリアの作成
<div class="results" id="results">
計算結果を表示するエリア全体を囲むdiv
要素です。<p>
各結果の項目(実働時間など)を表示するための段落です。<span id="actualDuration">0時間0分</span>
実際の計算結果(「0時間0分」の部分)がJavaScriptによって書き換えられる場所です。span
要素は、テキストの一部を囲んでスタイルを適用したり、今回のようにJavaScriptで内容を操作したりする際に便利です。
履歴表示エリアの作成
<div class="history-container">
履歴エリア全体を囲むdiv
要素です。<h2>計算履歴</h2>
履歴エリアのタイトルです。<ul id="history-list">
履歴のリストを表示するための「順序なしリスト」です。JavaScriptでここに<li>
要素を動的に追加していきます。<p class="no-history">まだ履歴がありません。</p>
履歴が一つもない場合に表示されるメッセージです。<button id="clear-history-btn">履歴をすべて削除</button>
履歴を全て消去するためのボタンです。
これで、ツールの見た目の骨格が完成しました!次にCSSで見た目を整えていきましょう。
まずは“見た目”から。仕組みはあとでじっくり!

CSSでデザインを整えよう(見た目の改善)
HTMLで骨格を作っただけでは、味気ない見た目です。次に、CSS(Cascading Style Sheets)を使って、ツールを見やすく、使いやすくするためのデザインを適用していきます。先ほどのHTMLの<head>
タグ内にある<style>
タグの中に、以下のCSSコードをコピー&ペーストしてください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 |
<style> body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #f4f7f6; color: #333; line-height: 1.6; margin: 0; padding: 20px; display: flex; flex-direction: column; align-items: center; } .container { background-color: #fff; padding: 30px; border-radius: 12px; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); max-width: 500px; width: 100%; box-sizing: border-box; margin-bottom: 20px; } h1 { color: #2c3e50; text-align: center; margin-bottom: 25px; font-size: 1.8em; border-bottom: 2px solid #e0e0e0; padding-bottom: 10px; } .input-group { margin-bottom: 20px; } .input-group label { display: block; margin-bottom: 8px; font-weight: bold; color: #555; font-size: 0.95em; } .input-group input[type="time"], .input-group input[type="number"] { width: calc(100% - 20px); /* Adjust for padding */ padding: 12px; border: 1px solid #ccc; border-radius: 8px; font-size: 1.1em; box-sizing: border-box; transition: border-color 0.3s; } .input-group input[type="time"]:focus, .input-group input[type="number"]:focus { border-color: #007bff; outline: none; } .checkbox-group { margin-bottom: 20px; display: flex; align-items: center; } .checkbox-group input[type="checkbox"] { margin-right: 8px; transform: scale(1.2); /* Make checkbox slightly larger */ } .checkbox-group label { font-weight: bold; color: #555; cursor: pointer; margin-bottom: 0; /* Override input-group label margin */ } button { display: block; width: 100%; padding: 15px; background-color: #007bff; color: white; border: none; border-radius: 8px; font-size: 1.2em; cursor: pointer; transition: background-color 0.3s ease; margin-top: 20px; } button:hover { background-color: #0056b3; } .results { background-color: #e9f7ef; /* Light green background for results */ border: 1px solid #d4edda; padding: 20px; border-radius: 8px; margin-top: 30px; font-size: 1.1em; line-height: 1.8; color: #218838; /* Dark green text */ } .results p { margin: 5px 0; font-weight: bold; } .results span { color: #004d00; /* Even darker green for values */ font-size: 1.2em; } .hourly-wage-group { display: flex; align-items: center; margin-top: 15px; gap: 10px; } .hourly-wage-group input { flex-grow: 1; width: auto; /* Override 100% width for flex */ } .hourly-wage-group span { font-weight: normal; color: #555; font-size: 0.95em; white-space: nowrap; /* Prevent "円" from wrapping */ } .history-container { background-color: #fff; padding: 30px; border-radius: 12px; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); max-width: 500px; width: 100%; box-sizing: border-box; margin-top: 20px; } .history-container h2 { color: #2c3e50; margin-bottom: 20px; font-size: 1.5em; border-bottom: 1px solid #e0e0e0; padding-bottom: 10px; } #history-list { list-style: none; padding: 0; max-height: 300px; overflow-y: auto; /* Scrollable history list */ border: 1px solid #eee; border-radius: 8px; } #history-list li { background-color: #f9f9f9; padding: 12px 15px; border-bottom: 1px solid #eee; display: flex; flex-direction: column; /* Stack details and actions on small screens */ gap: 8px; /* Space between elements in list item */ font-size: 0.95em; } #history-list li:last-child { border-bottom: none; } #history-list li .history-details { flex-grow: 1; line-height: 1.4; color: #444; } #history-list li .history-details strong { color: #000; font-weight: bold; } #history-list li .history-details span { display: inline-block; /* For better spacing */ margin-right: 10px; } #history-list li .history-actions { display: flex; gap: 8px; justify-content: flex-end; /* Align buttons to the right */ margin-top: 5px; /* Add some space above buttons */ } #history-list li button { width: auto; /* Don't stretch buttons */ padding: 6px 12px; font-size: 0.8em; margin: 0; border-radius: 5px; cursor: pointer; transition: background-color 0.2s; } #history-list li .load-btn { background-color: #28a745; /* Green for load */ color: white; } #history-list li .load-btn:hover { background-color: #218838; } #history-list li .delete-btn { background-color: #dc3545; /* Red for delete */ color: white; } #history-list li .delete-btn:hover { background-color: #c82333; } #clear-history-btn { background-color: #6c757d; /* Grey for clear all */ margin-top: 15px; font-size: 1em; padding: 10px; } #clear-history-btn:hover { background-color: #5a6268; } #history-list .no-history { text-align: center; color: #888; padding: 15px; } /* Responsive adjustments */ @media (max-width: 600px) { body { padding: 10px; } .container, .history-container { padding: 20px; } h1 { font-size: 1.5em; } button { font-size: 1.1em; padding: 12px; } .results { font-size: 1em; padding: 15px; } .results span { font-size: 1.1em; } #history-list li { flex-direction: column; /* Stack details and actions */ align-items: flex-start; } #history-list li .history-actions { width: 100%; justify-content: flex-end; /* Keep buttons to the right */ } } </style> |
基本的なCSSの適用方法
今回はHTMLファイル内の<head>
タグの中に直接<style>
タグを書いてCSSを記述しています。これは「埋め込みスタイルシート」と呼ばれる方法で、簡単なツールを作る際には手軽で便利です。
CSSは、HTML要素に「どんなスタイルを適用するか」を指示するものです。例えば、body { ... }
はページの背景色や文字色を設定し、.container { ... }
はメインの入力エリアの背景や影を設定しています。
主要な要素のスタイリング
body
と.container
ページ全体と、メインの入力フォーム部分のレイアウトや背景色、影などを設定しています。display: flex; flex-direction: column; align-items: center;
を使うことで、コンテンツを中央に配置しています。input
フィールド
入力欄のサイズ、余白(padding
)、角の丸み(border-radius
)、枠線などを調整し、見やすくしています。transition
プロパティは、入力欄がフォーカスされたとき(クリックされたときなど)に枠線の色がスムーズに変化するアニメーション効果を設定しています。button
計算ボタンや履歴の削除ボタンの色、サイズ、角の丸みなどを設定し、クリックできる要素であることを明確にしています。cursor: pointer;
で、マウスカーソルがボタンの上にきたときに指の形に変わるようにしています。.results
エリア
計算結果が表示される部分です。背景を薄い緑色にして、重要な情報であることを視覚的に強調しています。history-list
履歴リストの見た目を整えています。overflow-y: auto;
を設定することで、履歴が増えすぎた場合にスクロールして表示できるようにしています。history-list li
各履歴アイテムの背景色、パディング、下線、そしてFlexboxを使った内部の配置を設定しています。hourly-wage-group
時給入力欄と「円」の表示を横並びにするためにFlexboxを使っています。
レスポンシブデザインの基礎
CSSの最後にある@media (max-width: 600px) { ... }
という記述は、レスポンシブデザインを実現するためのものです。これは「画面の幅が600px以下の場合に、この中に書かれたCSSを適用する」という意味です。
これにより、スマートフォンなどの小さい画面でアクセスした場合に、各要素のサイズや配置が自動的に調整され、見やすく表示されるようになります。
これで、ツールの見た目がぐっと良くなりましたね!いよいよ、ツールの頭脳となるJavaScriptのコーディングに入っていきましょう。
少しのCSSで、グッと使いやすく・見やすくなります!

JavaScriptで計算ロジックと機能を追加しよう(ツールの心臓部)
いよいよ、このツールのメインとなるJavaScript(JS)の記述です。HTMLで作成した入力値を取得し、計算を行い、結果をHTMLに表示し、さらには履歴を保存する、といったすべての「動き」をJavaScriptで実装します。
先ほどのHTMLファイルの</body>
タグの直前にある<script>
タグの中に、以下のJavaScriptコードをコピー&ペーストしてください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 |
<script> const startTimeInput = document.getElementById('startTime'); const endTimeInput = document.getElementById('endTime'); const includeBreakCheckbox = document.getElementById('includeBreak'); const breakTimeGroup = document.getElementById('breakTimeGroup'); const breakTimeInput = document.getElementById('breakTime'); const hourlyWageInput = document.getElementById('hourlyWage'); const calculateBtn = document.getElementById('calculateBtn'); const actualDurationSpan = document.getElementById('actualDuration'); const pureDurationSpan = document.getElementById('pureDuration'); const hourlyWageResultDiv = document.getElementById('hourlyWageResult'); const wageAmountSpan = document.getElementById('wageAmount'); const historyList = document.getElementById('history-list'); const clearHistoryBtn = document.getElementById('clear-history-btn'); // --- 初期設定とイベントリスナー --- // 休憩時間入力欄の表示/非表示を切り替え includeBreakCheckbox.addEventListener('change', () => { breakTimeGroup.style.display = includeBreakCheckbox.checked ? 'block' : 'none'; performCalculationOnly(); // チェックボックス変更時も計算をトリガー }); // 計算ボタンのクリックイベント calculateBtn.addEventListener('click', performCalculationAndSaveHistory); // 入力値が変更されたらリアルタイムで計算(履歴保存はしない) [startTimeInput, endTimeInput, breakTimeInput, hourlyWageInput].forEach(input => { input.addEventListener('input', performCalculationOnly); }); clearHistoryBtn.addEventListener('click', clearAllHistory); // --- 時間計算ロジック(履歴保存しないバージョン)--- function performCalculationOnly() { const startStr = startTimeInput.value; const endStr = endTimeInput.value; const breakStr = breakTimeInput.value; const hourlyWage = parseFloat(hourlyWageInput.value); // 時刻が未入力の場合は計算せず、結果表示をリセット if (!startStr || !endStr) { actualDurationSpan.textContent = '0時間0分'; pureDurationSpan.textContent = '0時間0分'; hourlyWageResultDiv.style.display = 'none'; return; } const startDate = new Date(`2000/01/01 ${startStr}`); // 年月日は計算に影響しないダミー const endDate = new Date(`2000/01/01 ${endStr}`); // 終了時刻が開始時刻より前の場合は、日を跨いだと判断して翌日に設定 if (endDate < startDate) { endDate.setDate(endDate.getDate() + 1); } // 実働時間(ミリ秒) let actualDurationMs = endDate.getTime() - startDate.getTime(); // 休憩時間(ミリ秒)を計算 let breakDurationMs = 0; if (includeBreakCheckbox.checked && breakStr) { const breakParts = breakStr.split(':'); const breakHours = parseInt(breakParts[0] || '0'); // 未入力の場合を考慮 const breakMinutes = parseInt(breakParts[1] || '0'); breakDurationMs = (breakHours * 60 + breakMinutes) * 60 * 1000; } // 純作業時間(ミリ秒): 実働時間から休憩時間を引く。0未満にはしない。 let pureDurationMs = actualDurationMs; if (includeBreakCheckbox.checked) { pureDurationMs = Math.max(0, actualDurationMs - breakDurationMs); } // 結果を「〇時間〇分」形式で表示するユーティリティ関数 function formatDuration(ms) { const totalMinutes = Math.floor(ms / (60 * 1000)); const hours = Math.floor(totalMinutes / 60); const minutes = totalMinutes % 60; return `${hours}時間${minutes}分`; } // 結果表示を更新 actualDurationSpan.textContent = formatDuration(actualDurationMs); pureDurationSpan.textContent = formatDuration(pureDurationMs); // 時給換算の計算と表示 if (!isNaN(hourlyWage) && hourlyWage > 0) { const pureDurationHours = pureDurationMs / (1000 * 60 * 60); // 純作業時間を時間単位に変換 const calculatedWage = Math.round(hourlyWage * pureDurationHours); // 四捨五入 wageAmountSpan.textContent = `${calculatedWage}円`; hourlyWageResultDiv.style.display = 'block'; } else { hourlyWageResultDiv.style.display = 'none'; } } // --- 時間計算ロジック(履歴保存ありバージョン)--- function performCalculationAndSaveHistory() { // まずは計算のみ実行 performCalculationOnly(); // 現在の入力値と計算結果を履歴に保存 const startStr = startTimeInput.value; const endStr = endTimeInput.value; const breakStr = breakTimeInput.value; const hourlyWage = parseFloat(hourlyWageInput.value); // 時刻が未入力の場合は履歴に保存しない if (!startStr || !endStr) { alert('開始時刻と終了時刻を入力してください。'); return; } const currentHistoryEntry = { start: startStr, end: endStr, break: includeBreakCheckbox.checked ? breakStr : 'なし', actual: actualDurationSpan.textContent, // 計算された最終結果の文字列を保存 pure: pureDurationSpan.textContent, // 計算された最終結果の文字列を保存 hourlyWageInputVal: (!isNaN(hourlyWage) && hourlyWage > 0) ? hourlyWage : '' // 時給入力値を保存 }; saveHistory(currentHistoryEntry); } // --- 履歴機能 --- const HISTORY_KEY = 'workTimeHistory'; // ローカルストレージのキー // 履歴を読み込み、表示を更新する関数 function loadHistory() { const history = JSON.parse(localStorage.getItem(HISTORY_KEY) || '[]'); historyList.innerHTML = ''; // 一度履歴リストをクリア if (history.length === 0) { historyList.innerHTML = '<p class="no-history">まだ履歴がありません。</p>'; return; } // 履歴アイテムを逆順(最新が上)で追加 history.forEach((item, index) => { const li = document.createElement('li'); const wageDisplay = item.hourlyWageInputVal ? `<span>時給: <strong>${item.hourlyWageInputVal}円</strong></span>` : ''; li.innerHTML = ` <div class="history-details"> <span>開始: <strong>${item.start}</strong> - 終了: <strong>${item.end}</strong></span> ${item.break !== 'なし' ? `<span>休憩: ${item.break}</span>` : ''} <span>実働: <strong>${item.actual}</strong></span> <span>純作業: <strong>${item.pure}</strong></span> ${wageDisplay} </div> <div class="history-actions"> <button class="load-btn" data-index="${index}">読み込む</button> <button class="delete-btn" data-index="${index}">削除</button> </div> `; historyList.appendChild(li); }); addHistoryEventListeners(); // 各履歴アイテムにイベントリスナーを再設定 } // 履歴に新しいエントリを保存する関数 function saveHistory(entry) { const history = JSON.parse(localStorage.getItem(HISTORY_KEY) || '[]'); history.unshift(entry); // 最新のものを配列の先頭に追加 localStorage.setItem(HISTORY_KEY, JSON.stringify(history)); loadHistory(); // 履歴表示を更新 } // 履歴アイテムのボタンにイベントリスナーを設定する関数 function addHistoryEventListeners() { // 「読み込む」ボタンのイベント document.querySelectorAll('.load-btn').forEach(button => { button.onclick = (e) => { const index = parseInt(e.target.dataset.index); const history = JSON.parse(localStorage.getItem(HISTORY_KEY)); const item = history[index]; // 該当の履歴データを取得 // 入力欄にデータをセット startTimeInput.value = item.start; endTimeInput.value = item.end; // 休憩時間の読み込み if (item.break && item.break !== 'なし') { includeBreakCheckbox.checked = true; breakTimeInput.value = item.break; breakTimeGroup.style.display = 'block'; } else { includeBreakCheckbox.checked = false; breakTimeGroup.style.display = 'none'; breakTimeInput.value = '00:00'; // デフォルト値に戻す } // 時給入力値を読み込み hourlyWageInput.value = item.hourlyWageInputVal || ''; // 数値そのままセット performCalculationOnly(); // 読み込んだ値で計算をトリガー }; }); // 「削除」ボタンのイベント document.querySelectorAll('.delete-btn').forEach(button => { button.onclick = (e) => { if (!confirm('この履歴を削除してもよろしいですか?')) { return; // キャンセルされたら何もしない } const index = parseInt(e.target.dataset.index); let history = JSON.parse(localStorage.getItem(HISTORY_KEY)); history.splice(index, 1); // 該当インデックスの要素を削除 localStorage.setItem(HISTORY_KEY, JSON.stringify(history)); loadHistory(); // 履歴表示を更新 }; }); } // 全履歴を削除する関数 function clearAllHistory() { if (confirm('すべての履歴を削除してもよろしいですか?')) { localStorage.removeItem(HISTORY_KEY); loadHistory(); // 履歴表示をリセット } } // ページロード時の初期処理 window.addEventListener('load', () => { loadHistory(); // 既存の履歴を読み込む performCalculationOnly(); // 初期値で一度計算し、結果を表示する }); </script> |
ここがツールの頭脳!動く仕組みをつくっていきましょう!

JavaScriptファイルの読み込み
<script>
タグは、通常</body>
タグの直前に配置するのがベストプラクティスとされています。これは、HTML要素がすべて読み込まれてからJavaScriptが実行されるようにするためです。JavaScriptがHTML要素を操作しようとしたときに、まだその要素が読み込まれていない、というエラーを防ぐことができます。
要素の取得
JavaScriptでHTML要素を操作するには、まずその要素を「取得」する必要があります。 document.getElementById('ID名')
を使うと、指定したID
を持つHTML要素をJavaScriptの変数として取得できます。例えば、const startTimeInput = document.getElementById('startTime');
は、開始時刻の入力欄を取得してstartTimeInput
という変数に格納しています。
時刻の計算ロジック
JavaScriptでの時刻計算の肝となるのがDate
オブジェクトです。
new Date('YYYY/MM/DD HH:MM')
日付と時刻の文字列からDate
オブジェクトを作成します。今回は日付部分が計算に影響しないため、2000/01/01
のようなダミーの日付を使っています。getTime()
Date
オブジェクトが持つメソッドで、1970年1月1日00:00:00 UTCからの経過時間をミリ秒単位で返します。このミリ秒の差分を利用して時間を計算します。- 日を跨ぐ計算の対応
if (endDate < startDate) { endDate.setDate(endDate.getDate() + 1); }
の部分が、終了時刻が開始時刻よりも早い場合に、終了時刻を翌日に設定することで、日をまたいだ勤務時間も正しく計算できるようにしています。 - 休憩時間・時給換算のロジック:
includeBreakCheckbox.addEventListener('change', ...)
チェックボックスの状態が変更されたときに、休憩時間の入力欄の表示/非表示を切り替えるイベントリスナーを設定しています。- 休憩時間の文字列(例: "01:00")を「時間」と「分」に分割し、それらをミリ秒に変換して実働時間から差し引いています。
- 時給が入力されている場合、純作業時間を時間単位に変換し、時給を掛けることで報酬を算出しています。
Math.round()
で四捨五入しています。
performCalculationOnly()
この関数は、入力値が変更されたときや、履歴からデータを読み込んだときに、リアルタイムで計算結果を表示するために使われます(履歴は保存しません)。
リアルタイム計算とボタンクリックの連携
input.addEventListener('input', performCalculationOnly);
各入力欄に対してinput
イベントリスナーを設定しています。これにより、ユーザーが入力するたびにperformCalculationOnly
関数が呼び出され、リアルタイムで計算結果が更新されます。calculateBtn.addEventListener('click', performCalculationAndSaveHistory);
「計算する」ボタンがクリックされたときにperformCalculationAndSaveHistory
関数が呼び出されます。この関数は、計算結果を表示するだけでなく、その計算内容を履歴として保存する役割も担っています。
【重要】ローカルストレージを使った履歴保存機能
ブラウザのlocalStorage
は、Webサイトがユーザーのブラウザにデータを保存するための簡単な仕組みです。これにより、ブラウザを閉じたり、インターネット接続がなくてもデータを永続的に保存できます。
localStorage.setItem(キー, 値)
データを保存します。値は文字列である必要があるため、JavaScriptのオブジェクト(今回の履歴データ)はJSON.stringify()
を使ってJSON文字列に変換します。localStorage.getItem(キー)
保存されたデータを読み込みます。読み込んだデータは文字列なので、JSON.parse()
を使って元のJavaScriptオブジェクトに戻します。localStorage.removeItem(キー)
指定したキーのデータを削除します。history.unshift(entry)
新しい履歴エントリを配列の先頭に追加しています。これにより、最新の履歴がリストの最も上に表示されます。history.splice(index, 1)
配列から指定したインデックスの要素を削除します。loadHistory()
ローカルストレージから履歴データを読み込み、HTMLのリスト(ul#history-list
)に動的に<li>
要素を追加して表示を更新する関数です。addHistoryEventListeners()
loadHistory()
が呼ばれるたびに、新しく生成された履歴アイテム内の「読み込む」ボタンと「削除」ボタンにクリックイベントリスナーを再設定しています。これは、動的に追加された要素には自動的にイベントリスナーが付かないため、非常に重要な処理です。e.target.dataset.index
クリックされたボタンのdata-index
属性の値を取得し、どの履歴アイテムが操作されたかを識別しています。
clearAllHistory()
すべての履歴を削除する際に確認ダイアログ(confirm
)を表示し、誤操作を防いでいます。
これで、ツールのすべての機能が完成しました!ブラウザでwork_time_calculator.html
を開き、実際に使ってみてください。
ツール作りに役立つおすすめの学習リソース
「もっとWebツール開発を学びたい!」と感じたあなたのために、初心者でも安心して取り組めるおすすめの学習リソースを厳選してご紹介します。
書籍で学ぶならこちら!
確かな力が身につくJavaScript超入門
JavaScriptの基礎を体系的に学びたい方におすすめ。
初心者にもわかりやすい丁寧な解説が特徴で、文法だけでなく実践的な使い方も学べます。
スラスラわかるHTML&CSSのきほん
Webページの構造(HTML)と見た目(CSS)を、基礎からやさしく学べる一冊。
ツール作りに欠かせない「画面の作り方」が理解できます。
オンライン講座で効率的に学ぶ!
Udemy(ユーデミー)などのオンライン学習プラットフォーム
世界中の講師が教える動画講座が豊富にあります。自分のペースで学べ、より体系的に知識を深めたい場合におすすめです。
Web開発全般から、特定のJavaScriptフレームワークまで、幅広いコースが見つかります。
これらのリソースを活用して、あなたのWeb開発スキルをさらに向上させましょう!
まとめ
お疲れ様でした!
これであなただけの「シンプルな作業時間・休憩時間計算ツール」が完成しました!HTMLで骨格を作り、CSSで見た目を整え、そしてJavaScriptで計算ロジックと便利な履歴機能を実装しました。自分で書いたコードが実際に動くツールとして機能するのを見ると、大きな達成感がありますよね!
「自分のアイデアを形にする」ことほど、プログラミング学習のモチベーションになることはありません。
今回のツール作成を通して、「ちょっと不便」を「便利」に変えられる感動を、あなた自身で味わってもらえたなら嬉しいです。
【実際に作った時間計算ツールはこちらから使えます】▼