<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Mobile Ready TODO List with History</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Sortable/1.15.0/Sortable.min.js"></script>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background-color: #f0f2f5;
display: flex;
justify-content: center;
padding: 20px 10px;
margin: 0;
-webkit-tap-highlight-color: transparent;
}
.container {
width: 100%;
max-width: 500px;
background: #fff;
padding: 20px;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.header-left {
display: flex;
align-items: center;
gap: 10px;
}
h2 {
margin: 0;
color: #333;
font-size: 1.5rem;
}
.header-btn {
border: none;
padding: 6px 12px;
border-radius: 6px;
font-size: 16px; /* アイコンを少し大きく */
cursor: pointer;
color: #333;
transition: opacity 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.header-btn:active {
opacity: 0.8;
}
#edit-toggle-btn {
background-color: #6c757d;
color: white;
font-size: 14px;
}
#edit-toggle-btn.active {
background-color: #28a745;
}
#random-btn {
background-color: #ffc107;
}
/* ★ 履歴ボタンのスタイル */
#history-btn {
background-color: #6f42c1; /* 紫色 */
color: white;
}
.input-group {
display: flex;
margin-bottom: 20px;
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
border-radius: 8px;
overflow: hidden;
}
#task-input {
flex-grow: 1;
padding: 12px 15px;
border: 1px solid #ddd;
border-right: none;
outline: none;
font-size: 16px;
border-radius: 8px 0 0 8px;
}
#add-btn {
padding: 0 20px;
background-color: #007bff;
color: white;
border: none;
cursor: pointer;
font-weight: bold;
font-size: 16px;
border-radius: 0 8px 8px 0;
min-width: 60px;
}
.todo-list {
list-style: none;
padding: 0;
margin: 0;
min-height: 50px;
}
.todo-item {
background-color: #fff;
border-bottom: 1px solid #eee;
padding: 15px 10px;
display: flex;
justify-content: space-between;
align-items: center;
user-select: none;
touch-action: pan-y;
min-height: 40px;
}
.todo-text {
flex-grow: 1;
font-size: 16px;
padding-right: 10px;
word-break: break-all;
line-height: 1.5;
transition: color 0.3s;
}
.edit-input {
flex-grow: 1;
font-size: 16px;
padding: 5px 8px;
margin-right: 10px;
border: 2px solid #17a2b8;
border-radius: 4px;
outline: none;
width: 100px;
}
.handle, .delete-btn, .modify-btn {
display: none;
}
.edit-mode .handle {
display: inline-block;
color: #ccc;
margin-right: 10px;
cursor: grab;
font-size: 20px;
padding: 5px;
}
.btn-group {
display: flex;
gap: 5px;
align-items: center;
}
.edit-mode .modify-btn {
display: block;
background-color: #17a2b8;
color: white;
border: none;
padding: 8px 12px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
flex-shrink: 0;
}
.edit-mode .modify-btn.saving {
background-color: #007bff;
}
.edit-mode .delete-btn {
display: block;
background-color: #ff4d4d;
color: white;
border: none;
padding: 8px 12px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
flex-shrink: 0;
}
.sortable-ghost { opacity: 0.4; background-color: #e3f2fd; }
.sortable-drag { background: #fff; box-shadow: 0 5px 15px rgba(0,0,0,0.15); opacity: 1; }
/* ★ 履歴モーダルのスタイル */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
justify-content: center;
align-items: center;
}
.modal-content {
background-color: #fff;
padding: 20px;
border-radius: 12px;
width: 90%;
max-width: 450px;
max-height: 80vh;
display: flex;
flex-direction: column;
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
}
.modal-header h3 {
margin: 0;
font-size: 1.2rem;
color: #333;
}
.close-btn {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #666;
padding: 0 5px;
}
#history-list {
list-style: none;
padding: 0;
margin: 0;
overflow-y: auto;
flex-grow: 1;
}
.history-item {
padding: 10px 0;
border-bottom: 1px solid #f0f0f0;
font-size: 14px;
}
.history-date {
font-size: 12px;
color: #888;
margin-bottom: 4px;
}
.history-text {
color: #333;
font-weight: 500;
word-break: break-all;
}
.empty-history {
text-align: center;
color: #999;
padding: 20px 0;
}
#clear-history-btn {
margin-top: 15px;
background-color: #fff;
color: #dc3545;
border: 1px solid #dc3545;
padding: 8px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
width: 100%;
}
#clear-history-btn:active {
background-color: #f8d7da;
}
</style>
</head>
<body>
<div class="container" id="main-container">
<div class="header">
<div class="header-left">
<h2>My Tasks</h2>
<button id="random-btn" class="header-btn" title="ランダムに並び替え">🎲</button>
<button id="history-btn" class="header-btn" title="履歴を見る">📖</button>
</div>
<button id="edit-toggle-btn" class="header-btn">編集</button>
</div>
<div class="input-group">
<input type="text" id="task-input" placeholder="タスクを入力..." autocomplete="off">
<button id="add-btn">追加</button>
</div>
<ul id="todo-list" class="todo-list"></ul>
</div>
<div id="history-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>完了・削除履歴</h3>
<button class="close-btn" id="close-modal">×</button>
</div>
<ul id="history-list">
</ul>
<button id="clear-history-btn">履歴を全て消去</button>
</div>
</div>
<script>
const input = document.getElementById('task-input');
const addBtn = document.getElementById('add-btn');
const list = document.getElementById('todo-list');
const editToggleBtn = document.getElementById('edit-toggle-btn');
const randomBtn = document.getElementById('random-btn');
const historyBtn = document.getElementById('history-btn'); // ★
const container = document.getElementById('main-container');
// モーダル関連の要素
const modal = document.getElementById('history-modal');
const closeModalBtn = document.getElementById('close-modal');
const historyList = document.getElementById('history-list');
const clearHistoryBtn = document.getElementById('clear-history-btn');
// ★ 日時フォーマット用関数
function getCurrentTime() {
const now = new Date();
const y = now.getFullYear();
const m = (now.getMonth() + 1).toString().padStart(2, '0');
const d = now.getDate().toString().padStart(2, '0');
const h = now.getHours().toString().padStart(2, '0');
const min = now.getMinutes().toString().padStart(2, '0');
return `${y}/${m}/${d} ${h}:${min}`;
}
// ★ 履歴の保存
function saveToHistory(text) {
const historyData = JSON.parse(localStorage.getItem('myTodoHistory') || '[]');
const newItem = {
text: text,
date: getCurrentTime()
};
// 新しいものを先頭に
historyData.unshift(newItem);
localStorage.setItem('myTodoHistory', JSON.stringify(historyData));
}
// ★ 履歴の表示
function renderHistory() {
const historyData = JSON.parse(localStorage.getItem('myTodoHistory') || '[]');
historyList.innerHTML = '';
if (historyData.length === 0) {
historyList.innerHTML = '<li class="empty-history">履歴はありません</li>';
return;
}
historyData.forEach(item => {
const li = document.createElement('li');
li.className = 'history-item';
li.innerHTML = `
<div class="history-date">${item.date}</div>
<div class="history-text">${item.text}</div>
`;
historyList.appendChild(li);
});
}
// ★ モーダル操作
historyBtn.addEventListener('click', () => {
renderHistory();
modal.style.display = 'flex';
});
closeModalBtn.addEventListener('click', () => {
modal.style.display = 'none';
});
window.addEventListener('click', (e) => {
if (e.target === modal) {
modal.style.display = 'none';
}
});
clearHistoryBtn.addEventListener('click', () => {
if(confirm('履歴を全て完全に消去しますか?')) {
localStorage.removeItem('myTodoHistory');
renderHistory();
}
});
function saveTodos() {
const todos = [];
document.querySelectorAll('.todo-item').forEach(li => {
const inputEl = li.querySelector('.edit-input');
const textEl = li.querySelector('.todo-text');
if (inputEl) {
todos.push(inputEl.value);
} else if (textEl) {
todos.push(textEl.textContent);
}
});
localStorage.setItem('myTodoList', JSON.stringify(todos));
}
function loadTodos() {
const saved = localStorage.getItem('myTodoList');
if (saved) {
const todos = JSON.parse(saved);
todos.forEach(text => addTodo(text, false));
} else {
addTodo('タスク1', false);
addTodo('タスク2', false);
addTodo('タスク3', false);
saveTodos();
}
}
let sortable = new Sortable(list, {
animation: 150,
ghostClass: 'sortable-ghost',
dragClass: 'sortable-drag',
handle: '.handle',
delay: 0,
disabled: true,
onEnd: function() {
saveTodos();
}
});
randomBtn.addEventListener('click', () => {
const items = Array.from(list.children);
if (items.length < 2) return;
for (let i = items.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[items[i], items[j]] = [items[j], items[i]];
}
items.forEach(item => list.appendChild(item));
saveTodos();
});
editToggleBtn.addEventListener('click', () => {
const isEditMode = container.classList.toggle('edit-mode');
if (isEditMode) {
editToggleBtn.textContent = '完了';
editToggleBtn.classList.add('active');
sortable.option('disabled', false);
} else {
document.querySelectorAll('.modify-btn.saving').forEach(btn => btn.click());
editToggleBtn.textContent = '編集';
editToggleBtn.classList.remove('active');
sortable.option('disabled', true);
}
});
addBtn.addEventListener('click', () => {
const text = input.value.trim();
if (text) {
addTodo(text);
input.value = '';
input.focus();
}
});
input.addEventListener('keypress', (e) => {
if (e.key === 'Enter') addBtn.click();
});
function addTodo(text, save = true) {
const li = document.createElement('li');
li.classList.add('todo-item');
li.innerHTML = `
<span class="handle">≡</span>
<span class="todo-text"></span>
<div class="btn-group">
<button class="modify-btn">修正</button>
<button class="delete-btn">削除</button>
</div>
`;
const textSpan = li.querySelector('.todo-text');
textSpan.textContent = text;
const modifyBtn = li.querySelector('.modify-btn');
modifyBtn.addEventListener('click', () => {
const isEditing = modifyBtn.classList.contains('saving');
if (!isEditing) {
const currentText = textSpan.textContent;
const inputEl = document.createElement('input');
inputEl.type = 'text';
inputEl.value = currentText;
inputEl.classList.add('edit-input');
li.replaceChild(inputEl, textSpan);
modifyBtn.textContent = '保存';
modifyBtn.classList.add('saving');
inputEl.focus();
inputEl.addEventListener('keypress', (e) => {
if (e.key === 'Enter') modifyBtn.click();
});
} else {
const inputEl = li.querySelector('.edit-input');
const newText = inputEl.value.trim();
textSpan.textContent = inputEl.value === "" ? textSpan.textContent : newText;
li.replaceChild(textSpan, inputEl);
modifyBtn.textContent = '修正';
modifyBtn.classList.remove('saving');
saveTodos();
}
});
// ★ 削除ボタンの処理を変更
li.querySelector('.delete-btn').addEventListener('click', (e) => {
e.stopPropagation();
// 1. まず削除するかどうかの確認
if(!confirm('このタスクを削除しますか?')) {
return; // キャンセルの場合、何もしない
}
// 2. 履歴に残すかどうかの確認
// キャンセルボタンを押すと「記録しない(完全に削除)」という扱いにします
const shouldRecord = confirm('削除したタスクを履歴に記録しますか?\n\n[OK] 記録して削除\n[キャンセル] 記録せずに削除');
// 削除対象のテキストを取得(編集中ならinputの値、そうでなければspanのテキスト)
let taskText = "";
const inputEl = li.querySelector('.edit-input');
if (inputEl) {
taskText = inputEl.value;
} else {
taskText = textSpan.textContent;
}
// 記録する場合
if (shouldRecord && taskText) {
saveToHistory(taskText);
}
// 削除アニメーションと実行
li.style.transform = 'translateX(100%)';
li.style.transition = '0.3s';
setTimeout(() => {
li.remove();
saveTodos();
// 記録した場合は完了のフィードバックがあると親切かもしれません
// if(shouldRecord) alert('履歴に保存しました');
}, 300);
});
list.appendChild(li);
if (save) {
li.scrollIntoView({ behavior: 'smooth', block: 'end' });
saveTodos();
}
}
loadTodos();
</script>
</body>
</html>
使用変数
| ) { saveTodos -------( Function ) | |
| addBtn | |
| addTodo -------( Function ) | |
| autocomplete | |
| btn | |
| charset | |
| class | |
| className | |
| clearHistoryBtn | |
| closeModalBtn | |
| container | |
| content | |
| currentText | |
| d | |
| display | |
| editToggleBtn | |
| getCurrentTime -------( Function ) | |
| h | |
| historyBtn | |
| historyData | |
| historyList | |
| i | |
| id | |
| innerHTML | |
| input | |
| inputEl | |
| isEditing | |
| isEditMode | |
| item | |
| items | |
| j | |
| key | |
| lang | |
| length | |
| li | |
| list | |
| loadTodos -------( Function ) | |
| m | |
| min | |
| modal | |
| modifyBtn | |
| name | |
| newItem | |
| newText | |
| now | |
| placeholder | |
| randomBtn | |
| renderHistory -------( Function ) | |
| save | |
| saved | |
| saveTodos -------( Function ) | |
| saveToHistory -------( Function ) | |
| scalable | |
| scale | |
| shouldRecord | |
| sortable | |
| src | |
| target | |
| taskText | |
| text | |
| textContent | |
| textEl | |
| textSpan | |
| title | |
| todos | |
| transform | |
| transition | |
| type | |
| value | |
| width | |
| y |