Files

457 lines
16 KiB
JavaScript

/**
* App Kanban
*/
'use strict';
(async function () {
let boards;
const kanbanSidebar = document.querySelector('.kanban-update-item-sidebar'),
kanbanWrapper = document.querySelector('.kanban-wrapper'),
commentEditor = document.querySelector('.comment-editor'),
kanbanAddNewBoard = document.querySelector('.kanban-add-new-board'),
kanbanAddNewInput = [].slice.call(document.querySelectorAll('.kanban-add-board-input')),
kanbanAddBoardBtn = document.querySelector('.kanban-add-board-btn'),
datePicker = document.querySelector('#due-date'),
select2 = $('.select2'), // ! Using jquery vars due to select2 jQuery dependency
assetsPath = document.querySelector('html').getAttribute('data-assets-path');
// Init kanban Offcanvas
const kanbanOffcanvas = new bootstrap.Offcanvas(kanbanSidebar);
// Get kanban data
const kanbanResponse = await fetch(assetsPath + 'json/kanban.json');
if (!kanbanResponse.ok) {
console.error('error', kanbanResponse);
}
boards = await kanbanResponse.json();
// datepicker init
if (datePicker) {
datePicker.flatpickr({
monthSelectorType: 'static',
static: true,
altInput: true,
altFormat: 'j F, Y',
dateFormat: 'Y-m-d'
});
}
//! TODO: Update Event label and guest code to JS once select removes jQuery dependency
// select2
if (select2.length) {
function renderLabels(option) {
if (!option.id) {
return option.text;
}
var $badge = "<div class='badge " + $(option.element).data('color') + "'> " + option.text + '</div>';
return $badge;
}
select2.each(function () {
var $this = $(this);
$this.wrap("<div class='position-relative'></div>").select2({
placeholder: 'Select Label',
dropdownParent: $this.parent(),
templateResult: renderLabels,
templateSelection: renderLabels,
escapeMarkup: function (es) {
return es;
}
});
});
}
// Comment editor
if (commentEditor) {
new Quill(commentEditor, {
modules: {
toolbar: '.comment-toolbar'
},
placeholder: 'Write a Comment...',
theme: 'snow'
});
}
// Render board dropdown
const renderBoardDropdown = () => `
<div class="dropdown">
<i class="dropdown-toggle icon-base ti tabler-dots-vertical cursor-pointer"
id="board-dropdown"
data-bs-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false">
</i>
<div class="dropdown-menu dropdown-menu-end" aria-labelledby="board-dropdown">
<a class="dropdown-item delete-board" href="javascript:void(0)">
<i class="icon-base ti tabler-trash icon-xs"></i>
<span class="align-middle">Delete</span>
</a>
<a class="dropdown-item" href="javascript:void(0)">
<i class="icon-base ti tabler-edit icon-xs"></i>
<span class="align-middle">Rename</span>
</a>
<a class="dropdown-item" href="javascript:void(0)">
<i class="icon-base ti tabler-archive icon-xs"></i>
<span class="align-middle">Archive</span>
</a>
</div>
</div>
`;
// Render item dropdown
const renderDropdown = () => `
<div class="dropdown kanban-tasks-item-dropdown">
<i class="dropdown-toggle icon-base ti tabler-dots-vertical"
id="kanban-tasks-item-dropdown"
data-bs-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false">
</i>
<div class="dropdown-menu dropdown-menu-end" aria-labelledby="kanban-tasks-item-dropdown">
<a class="dropdown-item" href="javascript:void(0)">Copy task link</a>
<a class="dropdown-item" href="javascript:void(0)">Duplicate task</a>
<a class="dropdown-item delete-task" href="javascript:void(0)">Delete</a>
</div>
</div>
`;
// Render header
const renderHeader = (color, text) => `
<div class="d-flex justify-content-between flex-wrap align-items-center mb-2">
<div class="item-badges">
<div class="badge bg-label-${color}">${text}</div>
</div>
${renderDropdown()}
</div>
`;
// Render avatar
const renderAvatar = (images = '', pullUp = false, size = '', margin = '', members = '') => {
const transitionClass = pullUp ? ' pull-up' : '';
const sizeClass = size ? `avatar-${size}` : '';
const memberList = members ? members.split(',') : [];
return images
? images
.split(',')
.map((img, index, arr) => {
const marginClass = margin && index !== arr.length - 1 ? ` me-${margin}` : '';
const memberName = memberList[index] || '';
return `
<div class="avatar ${sizeClass}${marginClass} w-px-26 h-px-26"
data-bs-toggle="tooltip"
data-bs-placement="top"
title="${memberName}">
<img src="${assetsPath}img/avatars/${img}"
alt="Avatar"
class="rounded-circle${transitionClass}">
</div>
`;
})
.join('')
: '';
};
// Render footer
const renderFooter = (attachments, comments, assigned, members) => `
<div class="d-flex justify-content-between align-items-center flex-wrap mt-2">
<div class="d-flex">
<span class="d-flex align-items-center me-2">
<i class="icon-base ti tabler-paperclip me-1"></i>
<span class="attachments">${attachments}</span>
</span>
<span class="d-flex align-items-center ms-2">
<i class="icon-base ti tabler-message-2 me-1"></i>
<span>${comments}</span>
</span>
</div>
<div class="avatar-group d-flex align-items-center assigned-avatar">
${renderAvatar(assigned, true, 'xs', null, members)}
</div>
</div>
`;
// Initialize kanban
const kanban = new jKanban({
element: '.kanban-wrapper',
gutter: '12px',
widthBoard: '250px',
dragItems: true,
boards: boards,
dragBoards: true,
addItemButton: true,
buttonContent: '+ Add Item',
itemAddOptions: {
enabled: true,
content: '+ Add New Item',
class: 'kanban-title-button btn btn-default border-none',
footer: false
},
click: el => {
const element = el;
const title = element.getAttribute('data-eid')
? element.querySelector('.kanban-text').textContent
: element.textContent;
const date = element.getAttribute('data-due-date');
const dateObj = new Date();
const year = dateObj.getFullYear();
const dateToUse = date
? `${date}, ${year}`
: `${dateObj.getDate()} ${dateObj.toLocaleString('en', { month: 'long' })}, ${year}`;
const label = element.getAttribute('data-badge-text');
const avatars = element.getAttribute('data-assigned');
// Show kanban offcanvas
kanbanOffcanvas.show();
// Populate sidebar fields
kanbanSidebar.querySelector('#title').value = title;
kanbanSidebar.querySelector('#due-date').nextSibling.value = dateToUse;
// Using jQuery for select2
$('.kanban-update-item-sidebar').find(select2).val(label).trigger('change');
// Remove and update assigned avatars
kanbanSidebar.querySelector('.assigned').innerHTML = '';
kanbanSidebar.querySelector('.assigned').insertAdjacentHTML(
'afterbegin',
`${renderAvatar(avatars, false, 'xs', '1', el.getAttribute('data-members'))}
<div class="avatar avatar-xs ms-1">
<span class="avatar-initial rounded-circle bg-label-secondary">
<i class="icon-base ti tabler-plus icon-xs text-heading"></i>
</span>
</div>`
);
},
buttonClick: (el, boardId) => {
const addNewForm = document.createElement('form');
addNewForm.setAttribute('class', 'new-item-form');
addNewForm.innerHTML = `
<div class="mb-4">
<textarea class="form-control add-new-item" rows="2" placeholder="Add Content" autofocus required></textarea>
</div>
<div class="mb-4">
<button type="submit" class="btn btn-primary btn-sm me-3 waves-effect waves-light">Add</button>
<button type="button" class="btn btn-label-secondary btn-sm cancel-add-item waves-effect waves-light">Cancel</button>
</div>
`;
kanban.addForm(boardId, addNewForm);
addNewForm.addEventListener('submit', e => {
e.preventDefault();
const currentBoard = Array.from(document.querySelectorAll(`.kanban-board[data-id="${boardId}"] .kanban-item`));
kanban.addElement(boardId, {
title: `<span class="kanban-text">${e.target[0].value}</span>`,
id: `${boardId}-${currentBoard.length + 1}`
});
// Add dropdown to new tasks
const kanbanTextElements = Array.from(
document.querySelectorAll(`.kanban-board[data-id="${boardId}"] .kanban-text`)
);
kanbanTextElements.forEach(textElem => {
textElem.insertAdjacentHTML('beforebegin', renderDropdown());
});
// Prevent sidebar from opening on dropdown click
const newTaskDropdowns = Array.from(document.querySelectorAll('.kanban-item .kanban-tasks-item-dropdown'));
newTaskDropdowns.forEach(dropdown => {
dropdown.addEventListener('click', event => event.stopPropagation());
});
// Add delete functionality for new tasks
const deleteTaskButtons = Array.from(
document.querySelectorAll(`.kanban-board[data-id="${boardId}"] .delete-task`)
);
deleteTaskButtons.forEach(btn => {
btn.addEventListener('click', () => {
const taskId = btn.closest('.kanban-item').getAttribute('data-eid');
kanban.removeElement(taskId);
});
});
addNewForm.remove();
});
// Remove form on clicking cancel button
addNewForm.querySelector('.cancel-add-item').addEventListener('click', () => addNewForm.remove());
}
});
// Kanban Wrapper scrollbar
if (kanbanWrapper) {
new PerfectScrollbar(kanbanWrapper);
}
const kanbanContainer = document.querySelector('.kanban-container');
const kanbanTitleBoard = Array.from(document.querySelectorAll('.kanban-title-board'));
const kanbanItem = Array.from(document.querySelectorAll('.kanban-item'));
// Render custom items
if (kanbanItem.length) {
kanbanItem.forEach(el => {
const element = `<span class="kanban-text">${el.textContent}</span>`;
let img = '';
if (el.getAttribute('data-image')) {
img = `
<img class="img-fluid rounded mb-2"
src="${assetsPath}img/elements/${el.getAttribute('data-image')}">
`;
}
el.textContent = '';
if (el.getAttribute('data-badge') && el.getAttribute('data-badge-text')) {
el.insertAdjacentHTML(
'afterbegin',
`${renderHeader(el.getAttribute('data-badge'), el.getAttribute('data-badge-text'))}${img}${element}`
);
}
if (el.getAttribute('data-comments') || el.getAttribute('data-due-date') || el.getAttribute('data-assigned')) {
el.insertAdjacentHTML(
'beforeend',
renderFooter(
el.getAttribute('data-attachments') || 0,
el.getAttribute('data-comments') || 0,
el.getAttribute('data-assigned') || '',
el.getAttribute('data-members') || ''
)
);
}
});
}
// Initialize tooltips for rendered items
const tooltipTriggerList = Array.from(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
tooltipTriggerList.forEach(tooltipTriggerEl => {
new bootstrap.Tooltip(tooltipTriggerEl);
});
// Prevent sidebar from opening on dropdown button click
const tasksItemDropdown = Array.from(document.querySelectorAll('.kanban-tasks-item-dropdown'));
if (tasksItemDropdown.length) {
tasksItemDropdown.forEach(dropdown => {
dropdown.addEventListener('click', event => {
event.stopPropagation();
});
});
}
// Toggle "add new" input and actions for add-new-btn
if (kanbanAddBoardBtn) {
kanbanAddBoardBtn.addEventListener('click', () => {
kanbanAddNewInput.forEach(el => {
el.value = ''; // Clear input value
el.classList.toggle('d-none'); // Toggle visibility
});
});
}
// Render "add new" inline with boards
if (kanbanContainer) {
kanbanContainer.append(kanbanAddNewBoard);
}
// Makes kanban title editable for rendered boards
if (kanbanTitleBoard) {
kanbanTitleBoard.forEach(elem => {
elem.addEventListener('mouseenter', () => {
elem.contentEditable = 'true';
});
// Appends delete icon with title
elem.insertAdjacentHTML('afterend', renderBoardDropdown());
});
}
// Delete Board for rendered boards
const deleteBoards = Array.from(document.querySelectorAll('.delete-board'));
deleteBoards.forEach(elem => {
elem.addEventListener('click', () => {
const id = elem.closest('.kanban-board').getAttribute('data-id');
kanban.removeBoard(id);
});
});
// Delete task for rendered boards
const deleteTasks = Array.from(document.querySelectorAll('.delete-task'));
deleteTasks.forEach(task => {
task.addEventListener('click', () => {
const id = task.closest('.kanban-item').getAttribute('data-eid');
kanban.removeElement(id);
});
});
// Cancel "Add New Board" input
const cancelAddNew = document.querySelector('.kanban-add-board-cancel-btn');
if (cancelAddNew) {
cancelAddNew.addEventListener('click', () => {
kanbanAddNewInput.forEach(el => {
el.classList.toggle('d-none');
});
});
}
// Add new board
if (kanbanAddNewBoard) {
kanbanAddNewBoard.addEventListener('submit', e => {
e.preventDefault();
const value = e.target.querySelector('.form-control').value.trim();
const id = value.replace(/\s+/g, '-').toLowerCase();
kanban.addBoards([{ id, title: value }]);
// Add delete board option to new board and make title editable
const newBoard = document.querySelector('.kanban-board:last-child');
if (newBoard) {
const header = newBoard.querySelector('.kanban-title-board');
header.insertAdjacentHTML('afterend', renderBoardDropdown());
// Make title editable
header.addEventListener('mouseenter', () => {
header.contentEditable = 'true';
});
// Add delete functionality to new board
const deleteNewBoard = newBoard.querySelector('.delete-board');
if (deleteNewBoard) {
deleteNewBoard.addEventListener('click', () => {
const id = deleteNewBoard.closest('.kanban-board').getAttribute('data-id');
kanban.removeBoard(id);
});
}
}
// Hide input fields
kanbanAddNewInput.forEach(el => {
el.classList.add('d-none');
});
// Re-append the "Add New Board" form
if (kanbanContainer) {
kanbanContainer.append(kanbanAddNewBoard);
}
});
}
// Clear comment editor on Kanban sidebar close
kanbanSidebar.addEventListener('hidden.bs.offcanvas', () => {
const editor = kanbanSidebar.querySelector('.ql-editor').firstElementChild;
if (editor) editor.innerHTML = '';
});
// Re-init tooltip when offcanvas opens(Bootstrap bug)
if (kanbanSidebar) {
kanbanSidebar.addEventListener('shown.bs.offcanvas', () => {
const tooltipTriggerList = Array.from(kanbanSidebar.querySelectorAll('[data-bs-toggle="tooltip"]'));
tooltipTriggerList.forEach(tooltipTriggerEl => {
new bootstrap.Tooltip(tooltipTriggerEl);
});
});
}
})();