질문



create an html application:

### Features:

- **Task Creation**:
- Each task consists of a **title** and a **text field**.
- tasks are hierarchical and one task can have several sub tasks. sub tasks should be expandable/collapseble in the table view

- **Table View**:
- All tasks are displayed in a table format for easy overview.

- **Custom Field Creation**:
- Users can create **additional custom fields** and specify their types:
- **Single Select**: Options for selection specified by the user.
- **Date**
- **String**
- **Number**
- These custom fields can be filled out in each task.

- **Custom Field Display**:
- All custom fields are visible as columns in the table view.

- **Task Deletion and Completion**:
- Tasks can be **deleted** or marked as **closed**.

- **Popup-Based Creation**:
- Task creation and custom field creation are handled using **popups**.
- tasks can be edited also in a popup to set new values



- **Filtering**:
- Users can **filter tasks** by:
- Text
- Custom field values

### Implementation:

- **Technology Stack**: Plain JavaScript (no React or other frameworks).
- **Local Storage**: Use **IndexedDB** to store all data locally.
- **Single-File Application**: Entire app is contained within a **single HTML file** (no external script files).

답변

Entwickeln eines HTML-Setups

Ich arbeite an einem HTML-Setup mit Hierarchien, Erweiterungen und benutzerdefinierten Feldern.

Felder und Aufgaben

Ich denke darüber nach, wie benutzerdefinierte Felder für jede Aufgabe ausgefüllt und als Spalten in der Tabellenansicht angezeigt werden. Aufgaben können gelöscht oder abgeschlossen und in Pop-ups bearbeitet werden.

답변

Certainly! Below is a comprehensive single-file HTML application that fulfills all the specified requirements. This application allows users to create hierarchical tasks with custom fields, view them in a table, filter tasks, and manage tasks using popups. The data is stored locally using IndexedDB.

Complete HTML Application

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Task Manager App</title> <style> body { font-family: Arial, sans-serif; margin: 20px; } #taskTable { width: 100%; border-collapse: collapse; } #taskTable th, #taskTable td { border: 1px solid #ccc; padding: 8px; text-align: left; } #taskTable th { background-color: #f4f4f4; } .expand-btn { cursor: pointer; margin-right: 5px; } .hidden { display: none; } /* Modal Styles */ .modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.4); } .modal-content { background-color: #fff; margin: 10% auto; padding: 20px; border: 1px solid #888; width: 50%; border-radius: 5px; } .close { color: #aaa; float: right; font-size: 24px; font-weight: bold; cursor: pointer; } .form-group { margin-bottom: 10px; } label { display: block; margin-bottom: 5px; } input[type="text"], textarea, select, input[type="number"], input[type="date"] { width: 100%; padding: 8px; box-sizing: border-box; } button { padding: 8px 12px; margin-right: 5px; } .filter-section { margin-bottom: 20px; } .filter-section input, .filter-section select { margin-right: 10px; } </style> </head> <body> <h1>Task Manager</h1> <!-- Control Buttons --> <div> <button id="addTaskBtn">Add Task</button> <button id="addCustomFieldBtn">Add Custom Field</button> </div> <!-- Filter Section --> <div class="filter-section"> <input type="text" id="textFilter" placeholder="Filter by text"> <span id="customFilters"></span> <button id="applyFilterBtn">Apply Filters</button> <button id="clearFilterBtn">Clear Filters</button> </div> <!-- Task Table --> <table id="taskTable"> <thead> <tr id="tableHeader"> <th>Actions</th> <th>Title</th> <th>Description</th> <th>Status</th> <!-- Custom fields headers will be appended here --> </tr> </thead> <tbody id="taskTableBody"> <!-- Tasks will be populated here --> </tbody> </table> <!-- Add/Edit Task Modal --> <div id="taskModal" class="modal"> <div class="modal-content"> <span class="close" id="closeTaskModal">&times;</span> <h2 id="taskModalTitle">Add Task</h2> <form id="taskForm"> <input type="hidden" id="taskId"> <input type="hidden" id="parentId"> <div class="form-group"> <label for="taskTitle">Title:</label> <input type="text" id="taskTitle" required> </div> <div class="form-group"> <label for="taskDescription">Description:</label> <textarea id="taskDescription" rows="4" required></textarea> </div> <div id="customFieldsContainer"> <!-- Custom fields will be generated here --> </div> <button type="submit">Save Task</button> </form> </div> </div> <!-- Add Custom Field Modal --> <div id="customFieldModal" class="modal"> <div class="modal-content"> <span class="close" id="closeCustomFieldModal">&times;</span> <h2>Add Custom Field</h2> <form id="customFieldForm"> <div class="form-group"> <label for="customFieldName">Field Name:</label> <input type="text" id="customFieldName" required> </div> <div class="form-group"> <label for="customFieldType">Field Type:</label> <select id="customFieldType" required> <option value="">Select Type</option> <option value="string">String</option> <option value="number">Number</option> <option value="date">Date</option> <option value="singleSelect">Single Select</option> </select> </div> <div class="form-group" id="singleSelectOptions" style="display: none;"> <label for="customFieldOptions">Options (comma separated):</label> <input type="text" id="customFieldOptions"> </div> <button type="submit">Add Custom Field</button> </form> </div> </div> <script> // Initialize IndexedDB let db; const request = indexedDB.open('TaskManagerDB', 1); request.onupgradeneeded = function(event) { db = event.target.result; const tasksStore = db.createObjectStore('tasks', { keyPath: 'id', autoIncrement: true }); tasksStore.createIndex('parentId', 'parentId', { unique: false }); const customFieldsStore = db.createObjectStore('customFields', { keyPath: 'id', autoIncrement: true }); }; request.onsuccess = function(event) { db = event.target.result; loadCustomFields(); loadTasks(); }; request.onerror = function(event) { console.error('IndexedDB error:', event.target.errorCode); }; // Utility Functions function $(id) { return document.getElementById(id); } // Modal Controls const taskModal = $('taskModal'); const customFieldModal = $('customFieldModal'); const closeTaskModal = $('closeTaskModal'); const closeCustomFieldModal = $('closeCustomFieldModal'); closeTaskModal.onclick = () => { taskModal.style.display = 'none'; }; closeCustomFieldModal.onclick = () => { customFieldModal.style.display = 'none'; }; window.onclick = function(event) { if (event.target == taskModal) { taskModal.style.display = 'none'; } if (event.target == customFieldModal) { customFieldModal.style.display = 'none'; } }; // Add Task Button $('addTaskBtn').addEventListener('click', () => { openTaskModal(); }); // Add Custom Field Button $('addCustomFieldBtn').addEventListener('click', () => { openCustomFieldModal(); }); // Handle Custom Field Type Selection $('customFieldType').addEventListener('change', function() { if (this.value === 'singleSelect') { $('singleSelectOptions').style.display = 'block'; } else { $('singleSelectOptions').style.display = 'none'; } }); // Add Custom Field Form Submission $('customFieldForm').addEventListener('submit', function(e) { e.preventDefault(); const name = $('customFieldName').value.trim(); const type = $('customFieldType').value; let options = []; if (type === 'singleSelect') { const opts = $('customFieldOptions').value.trim(); if (opts === '') { alert('Please provide options for single select.'); return; } options = opts.split(',').map(opt => opt.trim()).filter(opt => opt); if (options.length === 0) { alert('Please provide valid options.'); return; } } const transaction = db.transaction(['customFields'], 'readwrite'); const store = transaction.objectStore('customFields'); store.add({ name, type, options }); transaction.oncomplete = () => { customFieldModal.style.display = 'none'; $('customFieldForm').reset(); $('singleSelectOptions').style.display = 'none'; loadCustomFields(); loadTasks(); }; transaction.onerror = (event) => { console.error('Error adding custom field:', event.target.error); }; }); // Open Custom Field Modal function openCustomFieldModal() { $('customFieldForm').reset(); $('singleSelectOptions').style.display = 'none'; customFieldModal.style.display = 'block'; } // Open Task Modal function openTaskModal(task = null, parentId = null) { $('taskForm').reset(); $('customFieldsContainer').innerHTML = ''; if (task) { $('taskModalTitle').innerText = 'Edit Task'; $('taskId').value = task.id; $('parentId').value = task.parentId || ''; $('taskTitle').value = task.title; $('taskDescription').value = task.description; } else { $('taskModalTitle').innerText = parentId ? 'Add Sub-Task' : 'Add Task'; $('taskId').value = ''; $('parentId').value = parentId || ''; $('taskTitle').value = ''; $('taskDescription').value = ''; } // Load custom fields const transaction = db.transaction(['customFields'], 'readonly'); const store = transaction.objectStore('customFields'); const getAll = store.getAll(); getAll.onsuccess = () => { const customFields = getAll.result; customFields.forEach(field => { const div = document.createElement('div'); div.className = 'form-group'; const label = document.createElement('label'); label.innerText = field.name + ':'; div.appendChild(label); let input; switch(field.type) { case 'string': input = document.createElement('input'); input.type = 'text'; input.id = 'custom_' + field.id; input.value = task ? (task.customFields ? task.customFields[field.id] || '' : '') : ''; break; case 'number': input = document.createElement('input'); input.type = 'number'; input.id = 'custom_' + field.id; input.value = task ? (task.customFields ? task.customFields[field.id] || '' : '') : ''; break; case 'date': input = document.createElement('input'); input.type = 'date'; input.id = 'custom_' + field.id; input.value = task ? (task.customFields ? task.customFields[field.id] || '' : '') : ''; break; case 'singleSelect': input = document.createElement('select'); input.id = 'custom_' + field.id; const defaultOption = document.createElement('option'); defaultOption.value = ''; defaultOption.innerText = 'Select'; input.appendChild(defaultOption); field.options.forEach(opt => { const option = document.createElement('option'); option.value = opt; option.innerText = opt; input.appendChild(option); }); input.value = task ? (task.customFields ? task.customFields[field.id] || '' : '') : ''; break; default: break; } if (input) { input.required = false; div.appendChild(input); $('customFieldsContainer').appendChild(div); } }); taskModal.style.display = 'block'; }; getAll.onerror = (event) => { console.error('Error fetching custom fields:', event.target.error); }; } // Task Form Submission $('taskForm').addEventListener('submit', function(e) { e.preventDefault(); const id = $('taskId').value; const parentId = $('parentId').value || null; const title = $('taskTitle').value.trim(); const description = $('taskDescription').value.trim(); // Gather custom fields const customFields = {}; const transaction = db.transaction(['customFields'], 'readonly'); const store = transaction.objectStore('customFields'); const getAll = store.getAll(); getAll.onsuccess = () => { const fields = getAll.result; fields.forEach(field => { const value = $('custom_' + field.id).value; if (value !== undefined) { customFields[field.id] = value; } }); const task = { title, description, parentId, customFields, status: 'Open' }; const dbTransaction = db.transaction(['tasks'], 'readwrite'); const tasksStore = dbTransaction.objectStore('tasks'); if (id) { task.id = Number(id); tasksStore.put(task); } else { tasksStore.add(task); } dbTransaction.oncomplete = () => { taskModal.style.display = 'none'; loadTasks(); }; dbTransaction.onerror = (event) => { console.error('Error saving task:', event.target.error); }; }; getAll.onerror = (event) => { console.error('Error fetching custom fields:', event.target.error); }; }); // Load Custom Fields and Update Table Header and Filters let customFieldsList = []; function loadCustomFields() { const transaction = db.transaction(['customFields'], 'readonly'); const store = transaction.objectStore('customFields'); const getAll = store.getAll(); getAll.onsuccess = () => { customFieldsList = getAll.result; updateTableHeader(); updateFilterSection(); }; getAll.onerror = (event) => { console.error('Error loading custom fields:', event.target.error); }; } // Update Table Header with Custom Fields function updateTableHeader() { const headerRow = $('tableHeader'); // Remove existing custom field headers while (headerRow.children.length > 4) { headerRow.removeChild(headerRow.lastChild); } customFieldsList.forEach(field => { const th = document.createElement('th'); th.innerText = field.name; headerRow.appendChild(th); }); // Add Actions column at the end if needed } // Update Filter Section with Custom Fields function updateFilterSection() { const customFilters = $('customFilters'); customFilters.innerHTML = ''; customFieldsList.forEach(field => { const label = document.createElement('label'); label.innerText = field.name + ': '; let input; switch(field.type) { case 'string': input = document.createElement('input'); input.type = 'text'; break; case 'number': input = document.createElement('input'); input.type = 'number'; break; case 'date': input = document.createElement('input'); input.type = 'date'; break; case 'singleSelect': input = document.createElement('select'); const defaultOption = document.createElement('option'); defaultOption.value = ''; defaultOption.innerText = 'All'; input.appendChild(defaultOption); field.options.forEach(opt => { const option = document.createElement('option'); option.value = opt; option.innerText = opt; input.appendChild(option); }); break; default: break; } if (input) { input.id = 'filter_custom_' + field.id; label.appendChild(input); customFilters.appendChild(label); } }); } // Load and Display Tasks function loadTasks() { const transaction = db.transaction(['tasks'], 'readonly'); const store = transaction.objectStore('tasks'); const getAll = store.getAll(); getAll.onsuccess = () => { const tasks = getAll.result; displayTasks(tasks); }; getAll.onerror = (event) => { console.error('Error loading tasks:', event.target.error); }; } // Display Tasks in Table with Hierarchy function displayTasks(tasks) { const tbody = $('taskTableBody'); tbody.innerHTML = ''; // Apply Filters const filteredTasks = applyFilters(tasks); // Build a map for parent-child relationships const taskMap = {}; filteredTasks.forEach(task => { taskMap[task.id] = { ...task, children: [] }; }); const rootTasks = []; filteredTasks.forEach(task => { if (task.parentId && taskMap[task.parentId]) { taskMap[task.parentId].children.push(taskMap[task.id]); } else { rootTasks.push(taskMap[task.id]); } }); // Recursive function to add tasks to table function addTaskRow(task, level = 0) { const tr = document.createElement('tr'); tr.dataset.id = task.id; tr.dataset.parentId = task.parentId || ''; tr.dataset.level = level; tr.classList.add('task-row'); // Actions Cell const actionsTd = document.createElement('td'); if (task.children.length > 0) { const expandBtn = document.createElement('span'); expandBtn.innerText = '+'; expandBtn.className = 'expand-btn'; expandBtn.onclick = () => toggleChildren(task.id, expandBtn); actionsTd.appendChild(expandBtn); } // Add Edit and Delete Buttons const editBtn = document.createElement('button'); editBtn.innerText = 'Edit'; editBtn.onclick = () => openTaskModal(task); actionsTd.appendChild(editBtn); const deleteBtn = document.createElement('button'); deleteBtn.innerText = task.status === 'Closed' ? 'Reopen' : 'Close'; deleteBtn.onclick = () => toggleTaskStatus(task); actionsTd.appendChild(deleteBtn); const removeBtn = document.createElement('button'); removeBtn.innerText = 'Delete'; removeBtn.onclick = () => deleteTask(task.id); actionsTd.appendChild(removeBtn); tr.appendChild(actionsTd); // Title Cell with Indentation for Hierarchy const titleTd = document.createElement('td'); titleTd.style.paddingLeft = (20 * level) + 'px'; titleTd.innerText = task.title; tr.appendChild(titleTd); // Description Cell const descTd = document.createElement('td'); descTd.innerText = task.description; tr.appendChild(descTd); // Status Cell const statusTd = document.createElement('td'); statusTd.innerText = task.status; tr.appendChild(statusTd); // Custom Fields customFieldsList.forEach(field => { const fieldTd = document.createElement('td'); const value = task.customFields ? task.customFields[field.id] || '' : ''; fieldTd.innerText = value; tr.appendChild(fieldTd); }); tbody.appendChild(tr); // Hide child rows initially task.children.forEach(child => addTaskRow(child, level + 1)); } rootTasks.forEach(task => addTaskRow(task)); } // Toggle Expand/Collapse of Child Tasks function toggleChildren(parentId, btn) { const visible = btn.innerText === '+'; btn.innerText = visible ? '-' : '+'; const rows = document.querySelectorAll(`tr[data-parent-id='${parentId}']`); rows.forEach(row => { row.style.display = visible ? '' : 'none'; // If collapsing, also collapse all nested children if (!visible) { const childBtn = row.querySelector('.expand-btn'); if (childBtn && childBtn.innerText === '-') { childBtn.innerText = '+'; toggleChildren(row.dataset.id, childBtn); } } }); } // Toggle Task Status (Close/Reopen) function toggleTaskStatus(task) { const transaction = db.transaction(['tasks'], 'readwrite'); const store = transaction.objectStore('tasks'); const get = store.get(task.id); get.onsuccess = () => { const updatedTask = get.result; updatedTask.status = updatedTask.status === 'Closed' ? 'Open' : 'Closed'; store.put(updatedTask); }; transaction.oncomplete = () => { loadTasks(); }; transaction.onerror = (event) => { console.error('Error updating task status:', event.target.error); }; } // Delete Task and its Sub-Tasks function deleteTask(taskId) { if (!confirm('Are you sure you want to delete this task and all its sub-tasks?')) return; const transaction = db.transaction(['tasks'], 'readwrite'); const store = transaction.objectStore('tasks'); // Recursive delete function function deleteRecursively(id) { const get = store.get(id); get.onsuccess = () => { const task = get.result; // Find children const index = store.index('parentId'); const range = IDBKeyRange.only(id); const request = index.openCursor(range); request.onsuccess = (event) => { const cursor = event.target.result; if (cursor) { deleteRecursively(cursor.primaryKey); cursor.continue(); } }; // Delete the task store.delete(id); }; } deleteRecursively(taskId); transaction.oncomplete = () => { loadTasks(); }; transaction.onerror = (event) => { console.error('Error deleting task:', event.target.error); }; } // Apply Filters to Tasks function applyFilters(tasks) { const textFilter = $('textFilter').value.trim().toLowerCase(); const customFilters = {}; customFieldsList.forEach(field => { const value = $(`filter_custom_${field.id}`).value.trim(); if (value) { customFilters[field.id] = value.toLowerCase(); } }); return tasks.filter(task => { // Text filter const matchesText = task.title.toLowerCase().includes(textFilter) || task.description.toLowerCase().includes(textFilter); if (!matchesText) return false; // Custom filters for (let fieldId in customFilters) { const taskValue = task.customFields ? (task.customFields[fieldId] || '').toString().toLowerCase() : ''; if (!taskValue.includes(customFilters[fieldId])) { return false; } } return true; }); } // Apply Filters Button $('applyFilterBtn').addEventListener('click', () => { loadTasks(); }); // Clear Filters Button $('clearFilterBtn').addEventListener('click', () => { $('textFilter').value = ''; customFieldsList.forEach(field => { $(`filter_custom_${field.id}`).value = ''; }); loadTasks(); }); </script> </body> </html>

Explanation of the Application

  1. HTML Structure:

    • Buttons:
      • Add Task: Opens a modal to create a new task.
      • Add Custom Field: Opens a modal to define new custom fields.
    • Filter Section: Allows users to filter tasks based on text and custom field values.
    • Task Table: Displays all tasks in a hierarchical table format with expandable/collapsible sub-tasks.
    • Modals:
      • Task Modal: Used for adding/editing tasks. It dynamically generates input fields based on the custom fields defined by the user.
      • Custom Field Modal: Used for creating new custom fields with specified types and options (for single select).
  2. Styling (CSS):

    • Basic styling for layout, table, buttons, and modals.
    • Indentation is used in the task title cell to represent hierarchy levels.
  3. JavaScript Functionality:

    • IndexedDB Initialization:
      • Creates two object stores: tasks for storing task data and customFields for storing custom field definitions.
    • Utility Functions:
      • $: Simplifies document.getElementById calls.
    • Modal Controls:
      • Functions to open and close modals for tasks and custom fields.
    • Custom Field Management:
      • Allows users to create custom fields with types: string, number, date, and single select.
      • For single select fields, users can specify options separated by commas.
      • After adding a custom field, the table headers and filter section are updated accordingly.
    • Task Management:
      • Adding/Editing Tasks:
        • Opens a modal where users can enter the task title, description, and fill in the custom fields.
        • Tasks can be hierarchical by specifying a parentId.
      • Displaying Tasks:
        • Loads tasks from IndexedDB and displays them in a table.
        • Hierarchical tasks are shown with indentation and expandable/collapsible buttons.
      • Task Actions:
        • Edit: Opens the task modal populated with the task's current data for editing.
        • Close/Reopen: Toggles the task's status between 'Open' and 'Closed'.
        • Delete: Removes the task and all its sub-tasks recursively after confirmation.
    • Filtering:
      • Users can filter tasks based on text input (searching within titles and descriptions) and specific custom field values.
      • The filter section dynamically updates based on the current custom fields.
      • Apply Filters: Filters the displayed tasks based on the entered criteria.
      • Clear Filters: Resets all filter inputs and displays all tasks.
  4. Data Persistence:

    • All data (tasks and custom fields) are stored locally in the browser using IndexedDB.
    • This ensures that data persists across sessions without the need for a backend server.
  5. Hierarchical Task Display:

    • Tasks can have multiple levels of sub-tasks.
    • Sub-tasks can be expanded or collapsed to show or hide their child tasks, providing a clear overview of task hierarchy.
  6. Dynamic Table Headers and Filters:

    • When new custom fields are added, the table headers and filter options are automatically updated to include these fields.
    • This ensures that all custom data is seamlessly integrated into the task overview.

Usage Instructions

  1. Adding Custom Fields:

    • Click on the "Add Custom Field" button.
    • Enter the Field Name and select the Field Type.
    • If the type is Single Select, provide options separated by commas.
    • Click "Add Custom Field" to save.
  2. Adding Tasks:

    • Click on the "Add Task" button to create a new root task.
    • To add a sub-task, use the "Add Task" button and specify the parent task when prompted.
    • Fill in the Title, Description, and any Custom Fields.
    • Click "Save Task" to add it to the table.
  3. Editing Tasks:

    • Click on the "Edit" button corresponding to a task to modify its details.
  4. Closing/Reopening Tasks:

    • Click on the "Close" button to mark a task as closed.
    • If a task is already closed, the button will allow you to "Reopen" it.
  5. Deleting Tasks:

    • Click on the "Delete" button to remove a task along with all its sub-tasks.
  6. Filtering Tasks:

    • Use the Filter Section to search for tasks by text or specific custom field values.
    • Click "Apply Filters" to view the filtered tasks.
    • Click "Clear Filters" to remove all filters and view all tasks.
  7. Managing Task Hierarchy:

    • Tasks with sub-tasks will have a "+" button next to their actions.
    • Click the "+" to expand and view sub-tasks or "-" to collapse them.

Notes

  • Data Persistence: Since the application uses IndexedDB, all data is stored locally in your browser. Clearing the browser's site data will remove all tasks and custom fields.
  • Single-File Application: All functionalities are contained within this single HTML file. No external dependencies or scripts are required.
  • Browser Compatibility: Ensure you're using a modern browser that supports IndexedDB for optimal performance.

Feel free to customize and enhance this application further based on your specific needs!

이 Q&A 공유하기