0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

カンバン方式 タスク管理ツール【自分用】

Last updated at Posted at 2025-03-18

image.png

・カンバン方式
・インストール不要。メモ帳とブラウザがあればどこでも動く
・エクスポート・インポート機能あり

kanban.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>カンバンタスク管理</title>
    <style>
        body {
            font-family: 'Segoe UI', 'Meiryo UI', sans-serif;
            margin: 20px;
            background-color: #f9f9f9;
            color: #333;
        }

        .controls {
            margin-bottom: 20px;
        }

        .controls button {
            background-color: #6d28d9;
            color: white;
            border: none;
            border-radius: 20px;
            padding: 8px 16px;
            font-size: 14px;
            cursor: pointer;
            margin-right: 10px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
            transition: all 0.2s ease;
        }

        .controls button:hover {
            background-color: #5b21b6;
            transform: translateY(-2px);
            box-shadow: 0 4px 6px rgba(0,0,0,0.1);
        }

        .container {
            display: flex;
            gap: 20px;
            overflow-x: auto;
            padding: 20px 0;
            scrollbar-width: thin;
            scrollbar-color: #6d28d9 #f0f0f0;
        }

        .container::-webkit-scrollbar {
            height: 8px;
        }

        .container::-webkit-scrollbar-track {
            background: #f0f0f0;
            border-radius: 4px;
        }

        .container::-webkit-scrollbar-thumb {
            background-color: #6d28d9;
            border-radius: 4px;
        }

        .column {
            background: #f3f4f6;
            border-radius: 12px;
            min-width: 300px;
            max-width: 300px;
            padding: 15px;
            box-shadow: 0 4px 6px rgba(0,0,0,0.05);
            border: 1px solid #e5e7eb;
        }

        .column-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 15px;
        }

        .column-header input {
            background: transparent;
            border: none;
            font-size: 16px;
            font-weight: bold;
            color: #4b5563;
            padding: 5px;
            border-radius: 4px;
            width: 80%;
        }

        .column-header input:focus {
            background: #ffffff;
            outline: 2px solid #6d28d9;
        }

        .column-header button {
            background: none;
            border: none;
            color: #9ca3af;
            font-size: 18px;
            cursor: pointer;
            padding: 0 5px;
            transition: color 0.2s;
        }

        .column-header button:hover {
            color: #ef4444;
        }

        .task {
            background: white;
            border-radius: 8px;
            padding: 12px;
            margin-bottom: 12px;
            cursor: move;
            box-shadow: 0 2px 4px rgba(0,0,0,0.05);
            transition: transform 0.15s, box-shadow 0.15s;
            border-left: 10px solid #6d28d9;
            word-wrap: break-word;
            overflow-wrap: break-word;
        }

        .task:hover {
            transform: translateY(-2px);
            box-shadow: 0 4px 8px rgba(0,0,0,0.1);
        }

        .task-content {
            margin-bottom: 12px;
        }

        .task-title {
            font-weight: bold;
            margin-bottom: 8px;
            font-size: 15px;
            color: #1f2937;
            overflow-wrap: break-word;
            word-wrap: break-word;
            hyphens: auto;
        }

        .task-memo {
            font-size: 13px;
            color: #6b7280;
            white-space: pre-wrap;
            margin-bottom: 10px;
            max-height: 100px;
            overflow-y: auto;
            overflow-wrap: break-word;
            word-wrap: break-word;
            hyphens: auto;
        }

        .task button {
            background-color: #f3f4f6;
            border: none;
            border-radius: 4px;
            padding: 5px 10px;
            font-size: 12px;
            cursor: pointer;
            transition: background 0.2s;
            margin-right: 5px;
        }

        .edit-btn {
            background-color: #8b5cf6 !important;
            color: white !important;
        }

        .edit-btn:hover {
            background-color: #7c3aed !important;
        }

        .task button:last-child:hover {
            background-color: #fecaca;
            color: #b91c1c;
        }

        .add-task-btn {
            width: 100%;
            padding: 8px;
            background-color: rgba(109, 40, 217, 0.1);
            color: #6d28d9;
            border: 1px dashed #6d28d9;
            border-radius: 8px;
            cursor: pointer;
            text-align: center;
            transition: all 0.2s;
            font-size: 14px;
        }

        .add-task-btn:hover {
            background-color: rgba(109, 40, 217, 0.2);
        }

        .modal {
            display: none;
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(0,0,0,0.5);
            z-index: 100;
        }

        .modal-content {
            background: white;
            padding: 25px;
            width: 500px;
            max-width: 90%;
            margin: 100px auto;
            border-radius: 12px;
            box-shadow: 0 10px 25px rgba(0,0,0,0.2);
            animation: modalFadeIn 0.3s;
        }

        @keyframes modalFadeIn {
            from { opacity: 0; transform: translateY(-20px); }
            to { opacity: 1; transform: translateY(0); }
        }

        .modal h3 {
            margin-top: 0;
            color: #1f2937;
            font-size: 18px;
            margin-bottom: 15px;
        }

        .modal input, .modal textarea {
            width: 100%;
            padding: 10px;
            border: 1px solid #d1d5db;
            border-radius: 6px;
            font-family: inherit;
            font-size: 14px;
            box-sizing: border-box;
        }

        .modal input:focus, .modal textarea:focus {
            outline: none;
            border-color: #8b5cf6;
            box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.2);
        }

        .modal textarea {
            height: 120px;
            resize: vertical;
            margin: 10px 0;
        }

        .color-palette {
            display: flex;
            gap: 12px;
            margin: 15px 0;
            flex-wrap: wrap;
        }
        
        .color-option {
            width: 36px;
            height: 36px;
            border: 2px solid #e5e7eb;
            border-radius: 50%;
            cursor: pointer;
            transition: transform 0.2s, border-color 0.2s;
        }
        
        .color-option:hover {
            transform: scale(1.1);
        }
        
        .color-option[data-color="#FFCCD0"] { background-color: #FFCCD0; }
        .color-option[data-color="#B5EAD7"] { background-color: #B5EAD7; }
        .color-option[data-color="#C7CEEA"] { background-color: #C7CEEA; }
        .color-option[data-color="#FDE9C2"] { background-color: #FDE9C2; }
        
        #editColor {
            width: 36px;
            height: 36px;
            border: 2px solid #e5e7eb;
            background: none;
            padding: 0;
            cursor: pointer;
            border-radius: 50%;
        }

        .modal-buttons {
            display: flex;
            justify-content: flex-end;
            gap: 10px;
            margin-top: 20px;
        }

        .modal-buttons button {
            padding: 8px 16px;
            border-radius: 6px;
            font-size: 14px;
            cursor: pointer;
            transition: all 0.2s;
        }

        .save-btn {
            background-color: #8b5cf6;
            color: white;
            border: none;
        }

        .save-btn:hover {
            background-color: #7c3aed;
        }

        .cancel-btn {
            background-color: #f3f4f6;
            color: #4b5563;
            border: 1px solid #d1d5db;
        }

        .cancel-btn:hover {
            background-color: #e5e7eb;
        }

        .task-placeholder {
            height: 4px;
            background: #8b5cf6;
            margin: 8px 0;
            visibility: hidden;
            border-radius: 2px;
        }

        .dragging {
            opacity: 0.5;
        }

        .drag-over {
            background-color: #f0f8ff;
        }

        /* カスタムスクロールバー */
        .task-memo::-webkit-scrollbar {
            width: 6px;
        }

        .task-memo::-webkit-scrollbar-track {
            background: #f1f1f1;
            border-radius: 3px;
        }

        .task-memo::-webkit-scrollbar-thumb {
            background: #c4b5fd;
            border-radius: 3px;
        }

        .task-memo::-webkit-scrollbar-thumb:hover {
            background: #a78bfa;
        }
    </style>
</head>
<body>
    <div class="controls">
        <button onclick="addColumn()">列を追加</button>
        <button onclick="exportData()">エクスポート</button>
        <input type="file" id="importFile" accept=".json" style="display: none;">
        <button onclick="document.getElementById('importFile').click()">インポート</button>
    </div>

    <div class="container" id="container"></div>

    <!-- モーダルウィンドウ -->
    <div id="taskModal" class="modal">
        <div class="modal-content">
            <h3>タスクの編集</h3>
            <input type="text" id="editTitle" placeholder="タイトル" maxlength="100">
            <textarea id="editMemo" placeholder="詳細メモ"></textarea>

            <div class="color-palette">
                <button class="color-option" data-color="#FFCCD0" title="淡いピンク"></button>
                <button class="color-option" data-color="#B5EAD7" title="淡いミント"></button>
                <button class="color-option" data-color="#C7CEEA" title="淡いパープル"></button>
                <button class="color-option" data-color="#FDE9C2" title="淡いイエロー"></button>
                <input type="color" id="editColor" title="カスタムカラー">
            </div>

            <div class="modal-buttons">
                <button class="cancel-btn" onclick="closeModal()">キャンセル</button>
                <button class="save-btn" onclick="saveTaskChanges()">保存</button>
            </div>
        </div>
    </div>

    <script>
        // カラーオプションクリック処理
        document.querySelectorAll('.color-option').forEach(btn => {
            btn.addEventListener('click', function() {
                const color = this.dataset.color;
                document.getElementById('editColor').value = color;
                // 選択状態の視覚的フィードバック
                document.querySelectorAll('.color-option').forEach(o => 
                    o.style.borderColor = '#e5e7eb');
                this.style.borderColor = '#6d28d9';
            });
        });

        // グローバル変数宣言
        let columns = JSON.parse(localStorage.getItem('kanban')) || [
            { id: 'col1', name: 'To Do', tasks: [] },
            { id: 'col2', name: 'Doing', tasks: [] },
            { id: 'col3', name: 'Done', tasks: [] }
        ];

        let draggedTask = null;
        let sourceColumn = null;
        let currentEditingTask = null;

        // データ保存関数
        function saveData() {
            localStorage.setItem('kanban', JSON.stringify(columns));
            render();
        }

        // レンダリング関数
        function render() {
            const container = document.getElementById('container');
            container.innerHTML = '';

            columns.forEach(column => {
                const colEl = document.createElement('div');
                colEl.className = 'column';
                colEl.dataset.columnId = column.id;
                colEl.ondragover = handleDragOver;
                colEl.ondrop = handleDrop;

                // 列ヘッダー
                const header = document.createElement('div');
                header.className = 'column-header';

                const nameInput = document.createElement('input');
                nameInput.value = column.name;
                nameInput.onchange = (e) => {
                    column.name = e.target.value;
                    saveData();
                };

                const deleteBtn = document.createElement('button');
                deleteBtn.textContent = '×';
                deleteBtn.onclick = () => {
                    if (column.tasks.length > 0) {
                        if (!confirm(`「${column.name}」列には${column.tasks.length}個のタスクがあります。\n削除してもよろしいですか?`)) {
                            return;
                        }
                    }
                    columns = columns.filter(c => c.id !== column.id);
                    saveData();
                };

                header.appendChild(nameInput);
                header.appendChild(deleteBtn);
                colEl.appendChild(header);

                // タスクリスト
                const taskList = document.createElement('div');
                taskList.className = 'task-list';
                column.tasks.forEach(task => {
                    const taskEl = document.createElement('div');
                    taskEl.className = 'task';
                    taskEl.draggable = true;
                    taskEl.dataset.taskId = task.id;
                    taskEl.style.borderLeftColor = task.color || '#6d28d9';
                    taskEl.ondragstart = handleDragStart;

                    // タスク内容
                    const content = document.createElement('div');
                    content.className = 'task-content';

                    const title = document.createElement('div');
                    title.className = 'task-title';
                    title.textContent = task.title;

                    const memo = document.createElement('div');
                    memo.className = 'task-memo';
                    memo.textContent = task.memo || '';

                    // 編集ボタン
                    const editBtn = document.createElement('button');
                    editBtn.textContent = '編集';
                    editBtn.className = 'edit-btn';
                    editBtn.onclick = () => openEditModal(task, column);

                    // 削除ボタン
                    const deleteTaskBtn = document.createElement('button');
                    deleteTaskBtn.textContent = '削除';
                    deleteTaskBtn.onclick = (e) => {
                        e.stopPropagation();
                        if (confirm('このタスクを削除してもよろしいですか?')) {
                            column.tasks = column.tasks.filter(t => t.id !== task.id);
                            saveData();
                        }
                    };

                    content.appendChild(title);
                    content.appendChild(memo);
                    taskEl.appendChild(content);
                    taskEl.appendChild(editBtn);
                    taskEl.appendChild(deleteTaskBtn);
                    taskList.appendChild(taskEl);
                });
                colEl.appendChild(taskList);

                // タスク追加ボタン
                const addTaskBtn = document.createElement('button');
                addTaskBtn.textContent = '+ タスク追加';
                addTaskBtn.className = 'add-task-btn';
                addTaskBtn.onclick = () => {
                    const newTask = {
                        id: Date.now().toString(),
                        title: '',
                        memo: '',
                        color: '#C7CEEA'
                    };
                    column.tasks.push(newTask);
                    saveData();
                    // 追加後すぐに編集モーダルを開く
                    setTimeout(() => {
                        openEditModal(newTask, column);
                    }, 100);
                };

                colEl.appendChild(addTaskBtn);
                container.appendChild(colEl);
            });
        }

        // ドラッグ&ドロップ処理
        function handleDragStart(e) {
            draggedTask = e.target.closest('.task');
            sourceColumn = draggedTask.closest('.column');
            draggedTask.classList.add('dragging');
            e.dataTransfer.effectAllowed = 'move';
        }

        function handleDragOver(e) {
            e.preventDefault();
            const targetColumn = e.target.closest('.column');
            const taskList = targetColumn.querySelector('.task-list') || targetColumn;

            // プレースホルダーの削除
            document.querySelectorAll('.task-placeholder').forEach(p => p.remove());

            // マウスの位置からの挿入位置を計算
            const afterElement = getDragAfterElement(targetColumn, e.clientY);
            
            // プレースホルダーの表示
            const placeholder = document.createElement('div');
            placeholder.className = 'task-placeholder';
            if (afterElement) {
                taskList.insertBefore(placeholder, afterElement);
            } else {
                taskList.appendChild(placeholder);
            }
            placeholder.style.visibility = 'visible';
        }

        function handleDrop(e) {
            e.preventDefault();
            const targetColumn = e.target.closest('.column');
            
            // プレースホルダーの削除
            document.querySelectorAll('.task-placeholder').forEach(p => p.remove());

            if (!targetColumn) return;

            const sourceCol = columns.find(c => c.id === sourceColumn.dataset.columnId);
            const targetCol = columns.find(c => c.id === targetColumn.dataset.columnId);
            const taskId = draggedTask.dataset.taskId;
            const task = sourceCol.tasks.find(t => t.id === taskId);

            // 挿入位置の決定
            const afterElement = getDragAfterElement(targetColumn, e.clientY);
            const insertIndex = afterElement && afterElement.dataset.taskId
                ? targetCol.tasks.findIndex(t => t.id === afterElement.dataset.taskId)
                : targetCol.tasks.length;

            // タスクの移動
            sourceCol.tasks = sourceCol.tasks.filter(t => t.id !== taskId);
            targetCol.tasks.splice(insertIndex, 0, task);

            draggedTask.classList.remove('dragging');
            saveData();
        }

        // 挿入位置検出用ヘルパー関数
        function getDragAfterElement(container, y) {
            const tasks = [...container.querySelectorAll('.task:not(.dragging)')];

            return tasks.reduce((closest, task) => {
                const box = task.getBoundingClientRect();
                const offset = y - box.top - box.height / 2;
                
                if (offset < 0 && offset > closest.offset) {
                    return { offset, element: task };
                } else {
                    return closest;
                }
            }, { offset: Number.NEGATIVE_INFINITY }).element;
        }

        // モーダル関連関数
        function openEditModal(task, column) {
            currentEditingTask = task;
            document.getElementById('editTitle').value = task.title;
            document.getElementById('editMemo').value = task.memo || '';
            document.getElementById('editColor').value = task.color || '#C7CEEA';
            
            // 現在の色がプリセットの場合のハイライト
            document.querySelectorAll('.color-option').forEach(btn => {
                btn.style.borderColor = btn.dataset.color === task.color ? '#6d28d9' : '#e5e7eb';
            });
            
            document.getElementById('taskModal').style.display = 'block';
            document.getElementById('editTitle').focus();
        }

        function closeModal() {
            document.getElementById('taskModal').style.display = 'none';
        }

        function saveTaskChanges() {
            if (!currentEditingTask) return;
            
            const newTitle = document.getElementById('editTitle').value.trim();
            if (!newTitle) {
                alert('タイトルは必須です');
                return;
            }

            currentEditingTask.title = newTitle;
            currentEditingTask.memo = document.getElementById('editMemo').value;
            currentEditingTask.color = document.getElementById('editColor').value;
            saveData();
            closeModal();
        }

        // その他の関数
        function addColumn() {
            const name = prompt('新しい列の名前を入力してください');
            if (name && name.trim()) {
                columns.push({
                    id: Date.now().toString(),
                    name: name.trim(),
                    tasks: []
                });
                saveData();
            }
        }

        function exportData() {
            const data = JSON.stringify(columns, null, 2);
            const blob = new Blob([data], { type: 'application/json' });
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = 'kanban-data.json';
            a.click();
            URL.revokeObjectURL(url);
        }

        document.getElementById('importFile').addEventListener('change', function(e) {
            const file = e.target.files[0];
            if (!file) return;
            
            const reader = new FileReader();
            reader.onload = function() {
                try {
                    const importedData = JSON.parse(reader.result);
                    if (Array.isArray(importedData)) {
                        columns = importedData;
                        saveData();
                        alert('データのインポートが完了しました');
                    } else {
                        alert('無効なデータ形式です');
                    }
                } catch (error) {
                    alert('データの読み込みに失敗しました: ' + error.message);
                }
            };
            reader.readAsText(file);
            e.target.value = '';
        });

        // キーボードショートカット
        document.addEventListener('keydown', function(e) {
            if (e.key === 'Escape') {
                closeModal();
            } else if (e.key === 'Enter' && e.ctrlKey && document.getElementById('taskModal').style.display === 'block') {
                saveTaskChanges();
            }
        });

        // クリック時のモーダル外閉じる
        window.addEventListener('click', function(e) {
            const modal = document.getElementById('taskModal');
            if (e.target === modal) {
                closeModal();
            }
        });

        // 初期表示
        render();
    </script>
</body>
</html>

0
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?