feat: add initial Kanblendar project with core features
Introduced the Kanblendar project, which combines a Kanban board and a daily calendar. Users can create, edit, and organize tasks in columns and time slots, with notification support for upcoming tasks. Added README.md with detailed usage instructions and LICENSE file for licensing under MIT. Created example files (HTML, CSS, JS) to illustrate the usage of Kanblendar, and essential styles and scripts for its functionality.
This commit is contained in:
commit
38d9d36312
7 changed files with 788 additions and 0 deletions
19
LICENSE
Normal file
19
LICENSE
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
Copyright (c) 2024 Kumi <kanblendar@kumi.email>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
150
README.md
Normal file
150
README.md
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
# Kanblendar
|
||||||
|
|
||||||
|
[![Support Private.coffee!](https://shields.private.coffee/badge/private.coffee-support%20us!-pink?logo=coffeescript)](https://private.coffee)
|
||||||
|
|
||||||
|
Kanblendar is a JavaScript class that blends the functionality of a Kanban board and a daily calendar. Users can create, edit, and organize tasks within columns and time slots, and receive notifications for upcoming tasks.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Plain JavaScript class with no dependencies
|
||||||
|
- Create, edit, and delete tasks
|
||||||
|
- Organize tasks into columns and time slots
|
||||||
|
- Drag and drop tasks between columns and time slots
|
||||||
|
- Set due times and receive notifications
|
||||||
|
- Visual feedback during drag-and-drop operations
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Clone the repository:
|
||||||
|
```sh
|
||||||
|
git clone https://git.private.coffee/kumi/kanblendar.git
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Navigate to the project directory:
|
||||||
|
```sh
|
||||||
|
cd kanblendar
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Open `example.html` in your web browser to start the example application.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Creating a Task
|
||||||
|
|
||||||
|
1. Click the "Create Task" button.
|
||||||
|
2. Fill out the task form with the title, description, due time, column, and notification time.
|
||||||
|
3. Click "Save" to create the task.
|
||||||
|
|
||||||
|
### Editing a Task
|
||||||
|
|
||||||
|
1. Click on an existing task to open the edit form.
|
||||||
|
2. Update the task details as needed.
|
||||||
|
3. Click "Save" to save the changes.
|
||||||
|
|
||||||
|
### Deleting a Task
|
||||||
|
|
||||||
|
1. Click on an existing task to open the edit form.
|
||||||
|
2. Click the "Delete" button to remove the task.
|
||||||
|
|
||||||
|
### Drag and Drop
|
||||||
|
|
||||||
|
- Drag tasks between columns or into specific time slots.
|
||||||
|
- Tasks will update their due time if moved into a time slot where the current due time is not valid.
|
||||||
|
|
||||||
|
## Notifications
|
||||||
|
|
||||||
|
- Kanblendar requests notification permissions on load.
|
||||||
|
- Notifications are shown for tasks based on the specified notification time.
|
||||||
|
- Notifications are automatically unscheduled if a task is modified or deleted.
|
||||||
|
- If notifications are not supported or denied, a alert will be shown instead.
|
||||||
|
|
||||||
|
## Programmatically Modifying the Kanblendar
|
||||||
|
|
||||||
|
You can programmatically create, edit, and delete tasks using JavaScript. Here are some examples:
|
||||||
|
|
||||||
|
### Creating a Task
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const kanblendar = new Kanblendar();
|
||||||
|
|
||||||
|
const title = 'New Task';
|
||||||
|
const description = 'This is a new task.';
|
||||||
|
const dueTime = '2023-10-31T14:00';
|
||||||
|
const column = 'backlog';
|
||||||
|
const notifyBefore = 10; // Notify 10 minutes before the due time
|
||||||
|
|
||||||
|
const newTask = kanblendar.createTaskElement(title, description, dueTime, column, notifyBefore);
|
||||||
|
kanblendar.moveTaskToColumn(newTask, column);
|
||||||
|
kanblendar.updateTaskLocation(newTask, dueTime);
|
||||||
|
kanblendar.scheduleNotification(title, description, new Date(dueTime), notifyBefore, newTask.id);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Editing a Task
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const taskId = 'task-1234567890'; // Replace with the actual task ID
|
||||||
|
const task = document.getElementById(taskId);
|
||||||
|
|
||||||
|
const newTitle = 'Updated Task';
|
||||||
|
const newDescription = 'This is an updated task.';
|
||||||
|
const newDueTime = '2023-10-31T15:00';
|
||||||
|
const newColumn = 'in-progress';
|
||||||
|
const newNotifyBefore = 15; // Notify 15 minutes before the due time
|
||||||
|
|
||||||
|
task.querySelector('.kanblendar-task-title').innerText = newTitle;
|
||||||
|
task.querySelector('.kanblendar-task-desc').innerText = newDescription;
|
||||||
|
task.dataset.dueTime = newDueTime;
|
||||||
|
task.dataset.column = newColumn;
|
||||||
|
task.dataset.notifyBefore = newNotifyBefore;
|
||||||
|
|
||||||
|
kanblendar.moveTaskToColumn(task, newColumn);
|
||||||
|
kanblendar.updateTaskLocation(task, newDueTime);
|
||||||
|
kanblendar.cancelNotification(task.id);
|
||||||
|
kanblendar.scheduleNotification(newTitle, newDescription, new Date(newDueTime), newNotifyBefore, task.id);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deleting a Task
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const taskId = 'task-1234567890'; // Replace with the actual task ID
|
||||||
|
const task = document.getElementById(taskId);
|
||||||
|
|
||||||
|
kanblendar.deleteTask(task);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Project Structure
|
||||||
|
|
||||||
|
- `kanblendar.css` - CSS styles
|
||||||
|
- `kanblendar.js` - JavaScript functionality
|
||||||
|
- `example.html` - Example HTML file
|
||||||
|
- `example.js` - Additional JavaScript code for the example
|
||||||
|
- `example.css` - Additional CSS styles for the example
|
||||||
|
|
||||||
|
### Adding Features
|
||||||
|
|
||||||
|
1. Clone the repository and create a new branch:
|
||||||
|
```sh
|
||||||
|
git checkout -b feature-branch
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Make your changes and commit them:
|
||||||
|
```sh
|
||||||
|
git commit -m "Add new feature"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Push the changes to the remote repository:
|
||||||
|
```sh
|
||||||
|
git push origin feature-branch
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Open a pull request to the main branch.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Contributions are welcome! Please open an issue or submit a pull request for any improvements or bug fixes.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
43
example.css
Normal file
43
example.css
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
padding: 1em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
header button {
|
||||||
|
margin: 1em;
|
||||||
|
padding: 0.5em 1em;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #4CAF50;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
display: flex;
|
||||||
|
padding: 1em;
|
||||||
|
padding-bottom: 4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
footer {
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
padding: 1em 0;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer a {
|
||||||
|
color: white;
|
||||||
|
}
|
33
example.html
Normal file
33
example.html
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Kanblendar</title>
|
||||||
|
<link rel="stylesheet" href="kanblendar.css">
|
||||||
|
<link rel="stylesheet" href="example.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<h1>Kanblendar</h1>
|
||||||
|
<button id="createTaskBtn">Create Task</button>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<section class="kanblendar" id="kanblendar"
|
||||||
|
data-columns="Backlog,In Progress,Done"
|
||||||
|
data-start-time="08:00"
|
||||||
|
data-end-time="18:00"
|
||||||
|
data-interval="60">
|
||||||
|
<!-- Kanban columns will be dynamically generated here -->
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
<footer>
|
||||||
|
<p>Brought to you by <a href="https://git.private.coffee/kumi/kanblendar">Kumi</a></p>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<!-- Optional: Developers can define their own modal here -->
|
||||||
|
|
||||||
|
<script src="kanblendar.js"></script>
|
||||||
|
<script src="example.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
3
example.js
Normal file
3
example.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
new Kanblendar();
|
||||||
|
});
|
129
kanblendar.css
Normal file
129
kanblendar.css
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
.kanblendar {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
gap: 1em;
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanblendar-column {
|
||||||
|
flex: 1;
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
padding: 1em;
|
||||||
|
border-radius: 5px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanblendar-column h2 {
|
||||||
|
text-align: center;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
padding-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanblendar-non-timed-tasks {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
min-height: 50px;
|
||||||
|
/* Ensure it has a minimum height */
|
||||||
|
background-color: #fafafa;
|
||||||
|
border: 1px dashed #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanblendar-task {
|
||||||
|
background-color: #fff;
|
||||||
|
margin: 0.5em 0;
|
||||||
|
padding: 0.5em;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 3px;
|
||||||
|
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.1);
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanblendar-time-slot {
|
||||||
|
background-color: #e9ecef;
|
||||||
|
margin: 0.5em 0;
|
||||||
|
padding: 1em;
|
||||||
|
border-radius: 5px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
text-align: center;
|
||||||
|
min-height: 2em;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanblendar-time-slot::after {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanblendar-drag-over {
|
||||||
|
background-color: #cce5ff;
|
||||||
|
/* Highlight color */
|
||||||
|
border-color: #004085;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal Styles */
|
||||||
|
.kanblendar-modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanblendar-modal-content {
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 2em;
|
||||||
|
border-radius: 5px;
|
||||||
|
width: 80%;
|
||||||
|
max-width: 500px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanblendar-close {
|
||||||
|
color: #aaa;
|
||||||
|
float: right;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanblendar-close:hover,
|
||||||
|
.kanblendar-close:focus {
|
||||||
|
color: black;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form Styles */
|
||||||
|
.kanblendar-modal-content form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanblendar-modal-content label,
|
||||||
|
.kanblendar-modal-content input,
|
||||||
|
.kanblendar-modal-content textarea,
|
||||||
|
.kanblendar-modal-content select,
|
||||||
|
.kanblendar-modal-content button {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanblendar-delete-task {
|
||||||
|
background-color: #ff4c4c;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 0.5em 1em;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
411
kanblendar.js
Normal file
411
kanblendar.js
Normal file
|
@ -0,0 +1,411 @@
|
||||||
|
class Kanblendar {
|
||||||
|
constructor(config = {}) {
|
||||||
|
this.kanbanSection = document.getElementById('kanblendar');
|
||||||
|
this.tasks = new Map();
|
||||||
|
this.notificationTimeouts = new Map();
|
||||||
|
this.currentTask = null;
|
||||||
|
|
||||||
|
// Read configuration from data attributes
|
||||||
|
const dataConfig = {
|
||||||
|
columns: this.kanbanSection.dataset.columns ? this.kanbanSection.dataset.columns.split(',') : undefined,
|
||||||
|
startTime: this.kanbanSection.dataset.startTime,
|
||||||
|
endTime: this.kanbanSection.dataset.endTime,
|
||||||
|
interval: this.kanbanSection.dataset.interval ? parseInt(this.kanbanSection.dataset.interval, 10) : undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
// Default configuration options
|
||||||
|
const defaultConfig = {
|
||||||
|
columns: ['Backlog', 'In Progress', 'Done'],
|
||||||
|
timeSlots: null,
|
||||||
|
startTime: '08:00',
|
||||||
|
endTime: '18:00',
|
||||||
|
interval: 60, // in minutes
|
||||||
|
generateModal: true, // Option to generate modal or not
|
||||||
|
currentDate: new Date().toISOString().split('T')[0] // Default to today's date
|
||||||
|
};
|
||||||
|
|
||||||
|
// Merge configurations
|
||||||
|
this.config = { ...defaultConfig, ...config, ...dataConfig };
|
||||||
|
this.timeSlots = this.config.timeSlots || this.generateTimeSlots(this.config.startTime, this.config.endTime, this.config.interval);
|
||||||
|
this.columns = this.config.columns;
|
||||||
|
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.createTaskBtn = document.getElementById('createTaskBtn');
|
||||||
|
this.kanbanSection = document.getElementById('kanblendar');
|
||||||
|
|
||||||
|
if (this.config.generateModal) {
|
||||||
|
this.createModal();
|
||||||
|
} else {
|
||||||
|
this.taskModal = document.getElementById('kanblendar-taskModal');
|
||||||
|
this.closeModal = document.querySelector('.kanblendar-close');
|
||||||
|
this.taskForm = document.getElementById('kanblendar-taskForm');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.createTaskBtn.addEventListener('click', () => this.openModal());
|
||||||
|
this.closeModal.addEventListener('click', () => this.closeModalFunc());
|
||||||
|
window.addEventListener('click', (event) => {
|
||||||
|
if (event.target === this.taskModal) {
|
||||||
|
this.closeModalFunc();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.taskForm.addEventListener('submit', (event) => this.saveTask(event));
|
||||||
|
|
||||||
|
document.addEventListener('taskMoved', (e) => {
|
||||||
|
console.log(`Task ${e.detail.taskId} moved to ${e.detail.newParent}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.generateKanbanColumns();
|
||||||
|
this.initDragAndDrop();
|
||||||
|
this.requestNotificationPermission();
|
||||||
|
}
|
||||||
|
|
||||||
|
createModal() {
|
||||||
|
const modalHTML = `
|
||||||
|
<div id="kanblendar-taskModal" class="kanblendar-modal">
|
||||||
|
<div class="kanblendar-modal-content">
|
||||||
|
<span class="kanblendar-close">×</span>
|
||||||
|
<h2 id="kanblendar-modalTitle">Create Task</h2>
|
||||||
|
<form id="kanblendar-taskForm">
|
||||||
|
<label for="kanblendar-taskTitle">Title:</label>
|
||||||
|
<input type="text" id="kanblendar-taskTitle" name="kanblendar-taskTitle" required>
|
||||||
|
<label for="kanblendar-taskDescription">Description:</label>
|
||||||
|
<textarea id="kanblendar-taskDescription" name="kanblendar-taskDescription" required></textarea>
|
||||||
|
<label for="kanblendar-taskDueTime">Due Time:</label>
|
||||||
|
<input type="datetime-local" id="kanblendar-taskDueTime" name="kanblendar-taskDueTime">
|
||||||
|
<label for="kanblendar-taskColumn">Column:</label>
|
||||||
|
<select id="kanblendar-taskColumn" name="kanblendar-taskColumn"></select>
|
||||||
|
<label for="kanblendar-taskNotify">Notify Before (minutes):</label>
|
||||||
|
<input type="number" id="kanblendar-taskNotify" name="kanblendar-taskNotify" min="0">
|
||||||
|
<button type="submit">Save</button>
|
||||||
|
<button type="button" id="kanblendar-deleteTaskBtn" class="kanblendar-delete-task" style="display: none;">Delete</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.insertAdjacentHTML('beforeend', modalHTML);
|
||||||
|
this.taskModal = document.getElementById('kanblendar-taskModal');
|
||||||
|
this.closeModal = document.querySelector('.kanblendar-close');
|
||||||
|
this.taskForm = document.getElementById('kanblendar-taskForm');
|
||||||
|
this.deleteTaskBtn = document.getElementById('kanblendar-deleteTaskBtn');
|
||||||
|
|
||||||
|
// Populate the column dropdown
|
||||||
|
const taskColumnSelect = document.getElementById('kanblendar-taskColumn');
|
||||||
|
this.columns.forEach(column => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = column.toLowerCase().replace(/\s+/g, '-');
|
||||||
|
option.text = column;
|
||||||
|
taskColumnSelect.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.deleteTaskBtn.addEventListener('click', () => {
|
||||||
|
if (this.currentTask) {
|
||||||
|
this.deleteTask(this.currentTask);
|
||||||
|
this.closeModalFunc();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
generateTimeSlots(startTime, endTime, interval) {
|
||||||
|
const timeSlots = [];
|
||||||
|
let currentTime = this.parseTime(startTime);
|
||||||
|
const end = this.parseTime(endTime);
|
||||||
|
|
||||||
|
while (currentTime < end) {
|
||||||
|
timeSlots.push({
|
||||||
|
display: this.formatTime(currentTime),
|
||||||
|
value: currentTime.toTimeString().slice(0, 5) // HH:MM format
|
||||||
|
});
|
||||||
|
currentTime = this.addMinutes(currentTime, interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
return timeSlots;
|
||||||
|
}
|
||||||
|
|
||||||
|
parseTime(time) {
|
||||||
|
const [hours, minutes] = time.split(':').map(Number);
|
||||||
|
return new Date(1970, 0, 1, hours, minutes);
|
||||||
|
}
|
||||||
|
|
||||||
|
formatTime(date) {
|
||||||
|
return new Intl.DateTimeFormat(navigator.language, {
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: 'numeric'
|
||||||
|
}).format(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
addMinutes(date, minutes) {
|
||||||
|
return new Date(date.getTime() + minutes * 60000);
|
||||||
|
}
|
||||||
|
|
||||||
|
generateKanbanColumns() {
|
||||||
|
this.columns.forEach(column => {
|
||||||
|
const columnElement = document.createElement('div');
|
||||||
|
columnElement.classList.add('kanblendar-column');
|
||||||
|
columnElement.id = column.toLowerCase().replace(/\s+/g, '-');
|
||||||
|
columnElement.innerHTML = `<h2>${column}</h2><div class="kanblendar-non-timed-tasks" id="${column.toLowerCase().replace(/\s+/g, '-')}-tasks"></div>`;
|
||||||
|
|
||||||
|
this.timeSlots.forEach(timeSlot => {
|
||||||
|
const timeSlotElement = document.createElement('div');
|
||||||
|
timeSlotElement.classList.add('kanblendar-time-slot');
|
||||||
|
timeSlotElement.innerText = timeSlot.display;
|
||||||
|
timeSlotElement.dataset.startTime = timeSlot.value;
|
||||||
|
columnElement.appendChild(timeSlotElement);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.kanbanSection.appendChild(columnElement);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
initDragAndDrop() {
|
||||||
|
const timeSlots = document.querySelectorAll('.kanblendar-time-slot, .kanblendar-non-timed-tasks');
|
||||||
|
timeSlots.forEach(slot => {
|
||||||
|
slot.addEventListener('dragover', (e) => this.dragOver(e));
|
||||||
|
slot.addEventListener('drop', (e) => this.drop(e));
|
||||||
|
slot.addEventListener('dragenter', (e) => this.dragEnter(e));
|
||||||
|
slot.addEventListener('dragleave', (e) => this.dragLeave(e));
|
||||||
|
});
|
||||||
|
|
||||||
|
const tasks = document.querySelectorAll('.kanblendar-task');
|
||||||
|
tasks.forEach(task => {
|
||||||
|
task.addEventListener('dragstart', (e) => this.dragStart(e));
|
||||||
|
task.addEventListener('click', (e) => this.openModal(task)); // Add click event to open modal for editing
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
dragStart(e) {
|
||||||
|
e.dataTransfer.setData('text/plain', e.target.id);
|
||||||
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
|
}
|
||||||
|
|
||||||
|
dragOver(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.dataTransfer.dropEffect = 'move';
|
||||||
|
}
|
||||||
|
|
||||||
|
dragEnter(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (e.target.classList.contains('kanblendar-task')) {
|
||||||
|
e.target.parentElement.classList.add('kanblendar-drag-over');
|
||||||
|
} else if (e.target.parentElement.classList.contains('kanblendar-task')) {
|
||||||
|
e.target.parentElement.parentElement.classList.add('kanblendar-drag-over');
|
||||||
|
} else {
|
||||||
|
e.target.classList.add('kanblendar-drag-over');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dragLeave(e) {
|
||||||
|
if (e.target.classList.contains('kanblendar-task')) {
|
||||||
|
e.target.parentElement.classList.remove('kanblendar-drag-over');
|
||||||
|
} else if (e.target.parentElement.classList.contains('kanblendar-task')) {
|
||||||
|
e.target.parentElement.parentElement.classList.remove('kanblendar-drag-over');
|
||||||
|
} else {
|
||||||
|
e.target.classList.remove('kanblendar-drag-over');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drop(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (e.target.classList.contains('kanblendar-task')) {
|
||||||
|
e.target.parentElement.classList.remove('kanblendar-drag-over');
|
||||||
|
} else if (e.target.parentElement.classList.contains('kanblendar-task')) {
|
||||||
|
e.target.parentElement.parentElement.classList.remove('kanblendar-drag-over');
|
||||||
|
} else {
|
||||||
|
e.target.classList.remove('kanblendar-drag-over');
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = e.dataTransfer.getData('text/plain');
|
||||||
|
const task = document.getElementById(id);
|
||||||
|
|
||||||
|
// Check if the drop target is a valid drop zone (time slot or non-timed tasks area)
|
||||||
|
let dropTarget = e.target;
|
||||||
|
if (dropTarget.classList.contains('kanblendar-task')) {
|
||||||
|
dropTarget = dropTarget.parentElement;
|
||||||
|
} else if (dropTarget.parentElement.classList.contains('kanblendar-task')) {
|
||||||
|
dropTarget = dropTarget.parentElement.parentElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dropTarget.classList.contains('kanblendar-time-slot') || dropTarget.classList.contains('kanblendar-non-timed-tasks')) {
|
||||||
|
dropTarget.appendChild(task);
|
||||||
|
this.emitTaskMovedEvent(task, dropTarget);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the task's due time if dropped in a time slot and the current due time is not valid for that slot
|
||||||
|
if (dropTarget.classList.contains('kanblendar-time-slot')) {
|
||||||
|
const startTime = dropTarget.dataset.startTime;
|
||||||
|
const dueTime = new Date(task.dataset.dueTime);
|
||||||
|
const slotStartTime = new Date(`${this.config.currentDate}T${startTime}:00`);
|
||||||
|
const slotEndTime = this.addMinutes(slotStartTime, this.config.interval);
|
||||||
|
|
||||||
|
if (!(dueTime >= slotStartTime && dueTime <= slotEndTime)) {
|
||||||
|
task.dataset.dueTime = slotStartTime.toISOString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
openModal(task = null) {
|
||||||
|
this.currentTask = task;
|
||||||
|
if (task) {
|
||||||
|
document.getElementById('kanblendar-modalTitle').innerText = 'Edit Task';
|
||||||
|
document.getElementById('kanblendar-taskTitle').value = task.querySelector('.kanblendar-task-title').innerText;
|
||||||
|
document.getElementById('kanblendar-taskDescription').value = task.querySelector('.kanblendar-task-desc').innerText;
|
||||||
|
document.getElementById('kanblendar-taskDueTime').value = task.dataset.dueTime || '';
|
||||||
|
document.getElementById('kanblendar-taskColumn').value = task.dataset.column || '';
|
||||||
|
document.getElementById('kanblendar-taskNotify').value = task.dataset.notifyBefore || '';
|
||||||
|
this.deleteTaskBtn.style.display = 'block'; // Show delete button when editing
|
||||||
|
} else {
|
||||||
|
document.getElementById('kanblendar-modalTitle').innerText = 'Create Task';
|
||||||
|
this.taskForm.reset();
|
||||||
|
this.deleteTaskBtn.style.display = 'none'; // Hide delete button when creating
|
||||||
|
}
|
||||||
|
this.taskModal.style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
closeModalFunc() {
|
||||||
|
this.taskModal.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
saveTask(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const title = document.getElementById('kanblendar-taskTitle').value;
|
||||||
|
const description = document.getElementById('kanblendar-taskDescription').value;
|
||||||
|
const dueTime = document.getElementById('kanblendar-taskDueTime').value;
|
||||||
|
const column = document.getElementById('kanblendar-taskColumn').value;
|
||||||
|
const notifyBefore = parseInt(document.getElementById('kanblendar-taskNotify').value, 10);
|
||||||
|
|
||||||
|
let newTask = null;
|
||||||
|
|
||||||
|
if (this.currentTask) {
|
||||||
|
this.currentTask.querySelector('.kanblendar-task-title').innerText = title;
|
||||||
|
this.currentTask.querySelector('.kanblendar-task-desc').innerText = description;
|
||||||
|
this.currentTask.dataset.dueTime = dueTime;
|
||||||
|
this.currentTask.dataset.column = column;
|
||||||
|
this.currentTask.dataset.notifyBefore = notifyBefore;
|
||||||
|
this.moveTaskToColumn(this.currentTask, column);
|
||||||
|
this.updateTaskLocation(this.currentTask, dueTime);
|
||||||
|
this.cancelNotification(this.currentTask.id);
|
||||||
|
} else {
|
||||||
|
newTask = this.createTaskElement(title, description, dueTime, column, notifyBefore);
|
||||||
|
this.moveTaskToColumn(newTask, column); // Correctly move to the selected column
|
||||||
|
this.updateTaskLocation(newTask, dueTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dueTime && notifyBefore >= 0) {
|
||||||
|
this.scheduleNotification(title, description, new Date(dueTime), notifyBefore, this.currentTask ? this.currentTask.id : newTask.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.closeModalFunc();
|
||||||
|
}
|
||||||
|
|
||||||
|
createTaskElement(title, description, dueTime, column, notifyBefore) {
|
||||||
|
const id = `task-${Date.now()}`;
|
||||||
|
const newTask = document.createElement('div');
|
||||||
|
newTask.classList.add('kanblendar-task');
|
||||||
|
newTask.setAttribute('draggable', 'true');
|
||||||
|
newTask.setAttribute('id', id);
|
||||||
|
newTask.dataset.dueTime = dueTime;
|
||||||
|
newTask.dataset.column = column;
|
||||||
|
newTask.dataset.notifyBefore = notifyBefore;
|
||||||
|
newTask.innerHTML = `
|
||||||
|
<div class="kanblendar-task-title">${title}</div>
|
||||||
|
<div class="kanblendar-task-desc">${description}</div>
|
||||||
|
`;
|
||||||
|
newTask.addEventListener('dragstart', (e) => this.dragStart(e));
|
||||||
|
newTask.addEventListener('click', (e) => this.openModal(newTask)); // Add click event to open modal for editing
|
||||||
|
this.tasks.set(id, { title, description, dueTime, notifyBefore });
|
||||||
|
return newTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
moveTaskToColumn(task, column) {
|
||||||
|
const columnTasks = document.getElementById(`${column}-tasks`);
|
||||||
|
if (columnTasks) {
|
||||||
|
columnTasks.appendChild(task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTaskLocation(task, dueTime) {
|
||||||
|
const taskDate = new Date(dueTime).toISOString().split('T')[0];
|
||||||
|
if (taskDate === this.config.currentDate) {
|
||||||
|
const taskTime = new Date(dueTime).toTimeString().slice(0, 5); // HH:MM format
|
||||||
|
let placedInTimeSlot = false;
|
||||||
|
|
||||||
|
document.querySelectorAll(`#${task.dataset.column} .kanblendar-time-slot`).forEach(timeSlotElement => {
|
||||||
|
const startTime = timeSlotElement.dataset.startTime;
|
||||||
|
const endTime = this.addMinutes(this.parseTime(startTime), this.config.interval).toTimeString().slice(0, 5);
|
||||||
|
if (taskTime >= startTime && taskTime < endTime) {
|
||||||
|
timeSlotElement.appendChild(task);
|
||||||
|
placedInTimeSlot = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!placedInTimeSlot) {
|
||||||
|
const columnTasks = document.getElementById(`${task.dataset.column}-tasks`);
|
||||||
|
columnTasks.appendChild(task);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const columnTasks = document.getElementById(`${task.dataset.column}-tasks`);
|
||||||
|
columnTasks.appendChild(task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteTask(task) {
|
||||||
|
this.cancelNotification(task.id);
|
||||||
|
task.remove();
|
||||||
|
this.tasks.delete(task.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleNotification(title, description, dueTime, notifyBefore, taskId) {
|
||||||
|
const notifyTime = new Date(dueTime.getTime() - notifyBefore * 60000);
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
if (notifyTime > now) {
|
||||||
|
const timeout = notifyTime.getTime() - now.getTime();
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
this.showNotification(title, description);
|
||||||
|
this.notificationTimeouts.delete(taskId);
|
||||||
|
}, timeout);
|
||||||
|
|
||||||
|
this.notificationTimeouts.set(taskId, timeoutId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelNotification(taskId) {
|
||||||
|
if (this.notificationTimeouts.has(taskId)) {
|
||||||
|
clearTimeout(this.notificationTimeouts.get(taskId));
|
||||||
|
this.notificationTimeouts.delete(taskId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showNotification(title, description) {
|
||||||
|
if (Notification.permission === 'granted') {
|
||||||
|
new Notification(title, { body: description });
|
||||||
|
} else {
|
||||||
|
alert(`Reminder: ${title}\n${description}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
requestNotificationPermission() {
|
||||||
|
if ('Notification' in window) {
|
||||||
|
Notification.requestPermission().then(permission => {
|
||||||
|
if (permission === 'granted') {
|
||||||
|
console.log('Notification permission granted.');
|
||||||
|
} else {
|
||||||
|
console.log('Notification permission denied.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
emitTaskMovedEvent(task, target) {
|
||||||
|
const event = new CustomEvent('taskMoved', {
|
||||||
|
detail: {
|
||||||
|
taskId: task.id,
|
||||||
|
newParent: target.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.dispatchEvent(event);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue